Skip to content

Conversation

lavignes
Copy link
Member

Description: As part of leveraging Envoy in AWS App Mesh, we implemented a gRPC credentials extension that uses AWS IAM auth. To be able to use authenticate gRPC requests with AWS IAM credentials, we needed to add a code to Envoy to sign HTTP requests. More high-level details in #5215.

I opened up a prior PR: #5546 which was admittedly too large and unwieldy. This is part 1 of a series of smaller patches needed to add our gRPC authentication extension to Envoy.

Risk Level: Low
Testing: Unit tests for this API. This has also been verified to work when communicating with App Mesh.
Docs Changes: N/A
Release Notes: N/A

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great in general, some comments on structure, implementation and Envoy style. This is definitely heading in the right direction, thanks for splitting up the PRs!

envoy_package()

envoy_cc_library(
name = "signer_lib",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: if this is going to only ever be a pure interface, signer_interface would be more consistent with other conventions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know. I thought that convention was only used the public headers. e.g. include/envoy.


const absl::optional<std::string>& secretAccessKey() const { return secret_access_key_; }

void setSessionToken(const std::string& session_token) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both setters and constructors for these fields? Could this be an immutable object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need them. I can remove them.


~Credentials() = default;

Credentials(const std::string& access_key_id, const std::string& secret_access_key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we tend to use absl::string_view rather than const std::string& in new code, as it's strictly more general. You can still turn this into a string copy below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy will suggest pass by value here and std::move below for constructors like this.
https://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html

By doing this you may save one copy if caller does move too.

public:
Credentials() = default;

~Credentials() = default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: probably don't need this if we're not doing polymorphic inheritance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the destructor, but I do use the no-args constructor.

namespace Common {
namespace Aws {

class RegionProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at how this works in #5546. This allows both static and environment to specify region. I guess my main question is "do we really need this?". Specifically, Envoy's Node ID includes locality information (and a variety of ways to specify, from bootstrap config to CLI). Can we pull region from that in the interest of economy of configuration mechanism and making this more consistent with the rest of Envoy config?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to eventually add a region provider impl that pulls the region from an AWS-endpoint, much like we pull the credentials.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the larger point is "shouldn't we be synthesizing the Node locality in the bootstrap or CLI for Envoys based on the AWS endpoint data source?" If you do that, there is no need to manage data sources for this information in the extension.

Credentials can also be placed in Node metadata, but the limitation there is that if they change dynamically during the lifetime of the Envoy, they can become stale. For region, this should be static for the lifetime of an Envoy.

Copy link
Member Author

@lavignes lavignes Jan 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not addressing that larger point. I think it makes sense. I'll pipe the node metadata through in the later PR. I'll eliminate this class, especially since it will probably just provide static data for the foreseeable future.

// Path
const auto* path_header = headers.Path();
if (path_header == nullptr || path_header->value().empty()) {
throw EnvoyException("Message is missing :path header");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think either the method or path should be missing by construction, so you can probably omit these.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there doesn't seem to be anything preventing someone from not setting the path or method. Due to the way I plan to use this in the gRPC credentials plugin, I'd like to ensure these are set instead of seg-faulting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking on the request receive path, but here you are creating a new request.

}
out << "\n" << signing_headers << "\n";
// Content Hash
out << content_hash;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be slightly more readable to use fmt::format here instead of streams, but up to you if you prefer streams.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, IIRC fmt::format is much faster than streams, if this is potentially being used in data path, format is better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is mostly just "\n" joining a bunch of strings. Though it is clearly not evident :). I'm updating it to use absl::StrJoin instead.

}

std::map<std::string, std::string>
SignerImpl::canonicalizeHeaders(const Http::HeaderMap& headers) const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in here is quite arcane; do you think we could split this out of a private method into a standalone utility and write some extensive tests for this? It seems a possible source of compliance bugs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. It is one of the trickier parts of the code. And even the public documentation of the canonicalization process glosses over some details.

RELEASE_ASSERT(len <= EVP_MAX_MD_SIZE, "Hmac length too large");
HMAC_CTX_cleanup(&ctx);
mac.resize(len);
return mac;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think hmac and hash are general enough to put in source/common/ssl/utility.cc or somewhere like that, this seems like something other folks might want to reuse.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good idea

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, @bdecoste is working on moving SSL library dependent stuff to extensions, to allow compiling out BoringSSL #5543 , I think this might be fine but not sure how SSL library specific this is (BoringSSL vs OpenSSL).

message_->headers().Authorization()->value().c_str());
}

TEST_F(SignerImplTest, ContentHeader) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add short // explanations of what each of these tests does above each TEST_F?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

@lizan lizan added the waiting label Jan 14, 2019

#include "extensions/filters/http/common/aws/credentials_provider.h"
#include "extensions/filters/http/common/aws/region_provider.h"
#include "extensions/filters/http/common/aws/signer.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have an abstract signing API in include/envoy somewhere, and have AwsSigner implement that as an extension?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense if we want a generic request signing extension. If @lavignes wants to take that on in this PR series, that's fine, otherwise we could leave a TODO and do the refactor when we have a second signing extension that wants to do something similar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted for the TODO. It'd be trivial to expose it in a public header in the future.

* Use absl::string_view more aggresively
* Removing Aws::RegionProvider
* Move SHA-256 Digest/HMAC utils to common/ssl/utility
* Removed setters from Aws::Credentials
* Remove usage of stringstream
* Use ConstSingleton for string constants
* Add test comments

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great, just some minor stuff and would like @PiotrSikora to take a quick pass on the BoringSSL side.

// Path
const auto* path_header = headers.Path();
if (path_header == nullptr || path_header->value().empty()) {
throw EnvoyException("Message is missing :path header");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking on the request receive path, but here you are creating a new request.

@@ -101,5 +105,43 @@ SystemTime Utility::getExpirationTime(const X509& cert) {
return std::chrono::system_clock::from_time_t(days * 24 * 60 * 60 + seconds);
}

std::vector<uint8_t> Utility::getSha256Digest(const Buffer::Instance& buffer) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PiotrSikora can you do a review pass on these two functions and their tests from a BoringSSL perspective? Thanks.

unsigned int len;
rc = HMAC_Final(&ctx, mac.data(), &len);
RELEASE_ASSERT(rc == 1, "Failed to finalize HMAC");
RELEASE_ASSERT(len <= EVP_MAX_MD_SIZE, "HMAC length too large");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this best an ASSERT? I.e. it can never happen? Same with the other length check above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the conventions of the other parts of envoy that used these types of SSL apis. I think these, for all intents and purposes, don't happen.

parts.emplace_back(signing_headers);
parts.emplace_back(content_hash);
return absl::StrJoin(parts, "\n");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one might also belong in the utility like canonical headers creation, it has some edge cases that would benefit from unit tests unless already well covered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have tests in the signer_tests specifically for this function but I can pull it out into a utility. I'm not sure what the convention is on util creation in envoy. Is it for any one function that is sufficiently complex on its own? Or is it just about reuse? I don't really see the request or header canonicalization being used outside of signing AWS requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of up to you largely, as long as you can provide adequate test coverage. The philosophy is at a bare minimum we have code coverage based on lines, but for complex/tricky code, it's often useful to ensure we can do behavioral/branch/functional coverage via unit tests. Some folks use testing peers, others code structure to do this. I personally don't like moving private to public methods in a class just for test, but I do like adding a logically separate utility class that has the methods as public where it makes sense.

DateFormatter long_date_formatter_;
DateFormatter short_date_formatter_;

std::string createContentHash(Http::Message& message) const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: generally we prefer methods before data members in each access scoped section, e.g. private here.

class Utility {
public:
/**
* Creates a canonicalized header map according to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain why we are doing this; it's just for signing, right? We never put these on the wire..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole canonicalization process is at the end of the day about consistently hashing HTTP requests. We do the exact same canonicalization upstream as part of our AuthN. This map is really just part of the input string to the hash we put in the Authorization header.

I'll explicitly say this is to be used as an input to the request canonicalization function. (Which I'll also move into this util)

[](const Http::HeaderEntry& entry, void* context) -> Http::HeaderMap::Iterate {
auto* map = static_cast<std::map<std::string, std::string>*>(context);
// Pseudo-headers should not be canonicalized
if (entry.key().c_str()[0] == ':') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an empty header in there somehow as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

}
return Http::HeaderMap::Iterate::Continue;
},
&out);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling this is more generally useful (without the quirks below), but it's fine to leave here, just thinking out loud.

} else {
out.emplace(Http::Headers::get().HostLegacy.get(), std::string(value));
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to completely trust you on the business logic here, code looks good.

@@ -22,6 +22,7 @@ namespace Logger {
// clang-format off
#define ALL_LOGGER_IDS(FUNCTION) \
FUNCTION(admin) \
FUNCTION(aws) \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to use http?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given where the code is now, it could just be classified as http. I'm fine doing that. :)

namespace Common {
namespace Aws {

void SignerImpl::sign(Http::Message& message) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole function assumes that the complete request body is available at this point. I'm not sure how are you going to use this, but Envoy doesn't buffer requests before forwarding them, so this won't work for proxied requests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point. In the sigv4 algorithm, the request body signing is technically optional (for this very reason), but adds an extra layer of security for certain types of requests. I'll add a parameter, much like we do in the official SDK to control signing the body. The caveat being, that the body must be fully available at that point.

unsigned int digest_length;
rc = EVP_DigestFinal(&ctx, digest.data(), &digest_length);
RELEASE_ASSERT(rc == 1, "Failed to finalize digest");
RELEASE_ASSERT(digest_length == SHA256_DIGEST_LENGTH, "Digest length mismatch");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we check this in a few other places, but it turns out this isn't necessary.

}

std::vector<uint8_t> Utility::getSha256Hmac(const std::vector<uint8_t>& key,
absl::string_view string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: string isn't very descriptive. Could you use message or data here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no problem

}

std::vector<uint8_t> Utility::getSha256Hmac(const std::vector<uint8_t>& key,
absl::string_view string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you can replace this whole function with:

std::vector<uint8_t> hmac(SHA256_DIGEST_LENGTH);
unsigned int len;
rc = HMAC(EVP_sha256(), key.data(), key.size(), message.data(), message.size(), hmac.data(), &len);
RELEASE_ASSERT(rc != nullptr, "Failed to create HMAC");
return hmac;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing

@@ -1,8 +1,12 @@
#include "common/ssl/utility.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This file doesn't exist anymore. @bdecoste moved TLS socket to extensions, so that it can be compiled out (and another TLS library can be used in it's place), it's new home is in source/extensions/transport_sockets/tls.

Having said that, I'd prefer if you created another target for this (e.g. source/extensions/common/crypto?), since those functions have nothing to do with SSL/TLS.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually thinking about making a "crypto" util but just ran with the suggestion to put it in common/ssl/utillty. Easy change :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be in core actually, since even core code might want to compute HMAC and SHAs. How about source/common/crypto?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I don't leverage any code that is different between Open/BoringSSL, but it would require some compile-time plumbing, which is not as clean as swapping in/out an extension, if neither lib is used or some other crypto impl was used instead.

How about I move this to source/common/crypto for now. And make an issue to propose adding a crypto impl extension api?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that sounds good. Would like to merge this PR soon, so whatever is reasonable here and we can then look at the next one, thanks.

* Remove Logging::Id::aws
* Move crypto utils to Extensions::Common::Crypto
* Make request body signing optional
* Move request canonicalization method to Aws::Utility
* Explicity check for empty headers when canonicalizing

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Signed-off-by: Scott LaVigne <lavignes@amazon.com>
absl::string_view message) {
std::vector<uint8_t> hmac(SHA256_DIGEST_LENGTH);
unsigned int len;
auto ret =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: const

parts.emplace_back(signing_headers);
parts.emplace_back(content_hash);
return absl::StrJoin(parts, "\n");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of up to you largely, as long as you can provide adequate test coverage. The philosophy is at a bare minimum we have code coverage based on lines, but for complex/tricky code, it's often useful to ensure we can do behavioral/branch/functional coverage via unit tests. Some folks use testing peers, others code structure to do this. I personally don't like moving private to public methods in a class just for test, but I do like adding a logically separate utility class that has the methods as public where it makes sense.

Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo remaining comments, thanks!

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
@htuch
Copy link
Member

htuch commented Jan 22, 2019

LGTM, @PiotrSikora WDYT?

PiotrSikora
PiotrSikora previously approved these changes Jan 23, 2019
Copy link
Contributor

@PiotrSikora PiotrSikora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, sans the typo.

namespace Common {
namespace Crypto {

TEST(UtilityTest, TestSha256Disgest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: s/Disgest/Digest/g (here and in the 2 tests below).

* github/master: (78 commits)
  transport capture: rename to tap (#5666)
  Misc additions for stubbing out QUICHE platform impls. (#5684)
  router: fix http/2 shadow request with body. (#5674)
  jwt_authn: cleanup, separate verifier from the matcher (#5689)
  redis: add latency stats for commands (#5593)
  config: invoke initialization callbacks on remote close (#5671)
  ext_authz: v2Alpha migration and documentation improvements (#5672)
  Update Datadog tracer version to v0.4.1 (#5691)
  envoy: basic spellcheck (#5688)
  fix a google string vs std::string issue for easier import (#5685)
  tools: tagging deprecation issues as no stalebot (#5680)
  health filter: cache degraded health checks (#5659)
  upstream: fix degraded health check and thread posting (#5662)
  tracing: inject tracing context in router (#5661)
  mysql_utils.cc: access EnvoyException by reference. (#5669)
  tap: introduce HTTP tap filter (#5515)
  fix a comment error (#5664)
  docs: add relase note for degraded health (#5653)
  Add yes flag for gcc-7 apt-repository (#5657)
  test: fix ssl_integration_test flake. (#5650)
  ...

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@htuch htuch merged commit 040acac into envoyproxy:master Jan 23, 2019
danzh2010 pushed a commit to danzh2010/envoy that referenced this pull request Jan 24, 2019
As part of leveraging Envoy in AWS App Mesh, we implemented a gRPC credentials extension that uses AWS IAM auth. To be able to use authenticate gRPC requests with AWS IAM credentials, we needed to add a code to Envoy to sign HTTP requests. More high-level details in envoyproxy#5215.

I opened up a prior PR: envoyproxy#5546 which was admittedly too large and unwieldy. This is part 1 of a series of smaller patches needed to add our gRPC authentication extension to Envoy.

Risk Level: Low
Testing: Unit tests for this API. This has also been verified to work when communicating with App Mesh.

Signed-off-by: Scott LaVigne <lavignes@amazon.com>

Signed-off-by: Dan Zhang <danzh@google.com>
danzh2010 pushed a commit to danzh2010/envoy that referenced this pull request Jan 31, 2019
As part of leveraging Envoy in AWS App Mesh, we implemented a gRPC credentials extension that uses AWS IAM auth. To be able to use authenticate gRPC requests with AWS IAM credentials, we needed to add a code to Envoy to sign HTTP requests. More high-level details in envoyproxy#5215.

I opened up a prior PR: envoyproxy#5546 which was admittedly too large and unwieldy. This is part 1 of a series of smaller patches needed to add our gRPC authentication extension to Envoy.

Risk Level: Low
Testing: Unit tests for this API. This has also been verified to work when communicating with App Mesh.

Signed-off-by: Scott LaVigne <lavignes@amazon.com>

Signed-off-by: Scott LaVigne <1406859+lavignes@users.noreply.github.com>
@lavignes lavignes deleted the aws-sigv4-signer branch February 5, 2019 18:45
fredlas pushed a commit to fredlas/envoy that referenced this pull request Mar 5, 2019
As part of leveraging Envoy in AWS App Mesh, we implemented a gRPC credentials extension that uses AWS IAM auth. To be able to use authenticate gRPC requests with AWS IAM credentials, we needed to add a code to Envoy to sign HTTP requests. More high-level details in envoyproxy#5215.

I opened up a prior PR: envoyproxy#5546 which was admittedly too large and unwieldy. This is part 1 of a series of smaller patches needed to add our gRPC authentication extension to Envoy.

Risk Level: Low
Testing: Unit tests for this API. This has also been verified to work when communicating with App Mesh.

Signed-off-by: Scott LaVigne <lavignes@amazon.com>
Signed-off-by: Fred Douglas <fredlas@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants