Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce new aws-http-auth module which implements sigv4 and sigv4a #541

Merged
merged 6 commits into from
Sep 25, 2024

Conversation

lucix-aws
Copy link
Contributor

@lucix-aws lucix-aws commented Sep 25, 2024

Closes #473
Closes #474
Closes aws/aws-sdk-go-v2#2634

@lucix-aws lucix-aws requested review from a team as code owners September 25, 2024 16:10
}

// uriEncode implements "Amazon-style" URL escaping.
func uriEncode(path string, encodeSep bool) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

I only see this being called from buildCanonicalRequest and encodeSep is always set to false. Is there another future use that will use encodeSep as true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it's not necessary here. This was lifted from smithy-go httpbinding -- will remove it here.

QueueName: queueName,
})
if err != nil {
t.Fatalf("list queues: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this is a create queue error, not a list queue error

}
}

func TestBuildCanonicalRequest_DoubleHeader(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we also add a test for double header value, like x-amz-meta=a,b?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's a double header in one of these cases but i'll make it its own thing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wait--isn't this that? x-amz-foo has two values in this one.


rs, ok := s.Request.Body.(io.ReadSeeker)
if !ok || s.Options.DisableImplicitPayloadHashing {
s.PayloadHash = []byte(v4.UnsignedPayload)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't a request with no payload needs to hash the empty string instead of the magic string?

If there is no payload in the request, you compute a hash of the empty string, such as when you retrieve an object by using a GET request, there is nothing in the payload.
Hex(SHA256Hash(""))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes but that's not what this case is checking. This is just saying "if we can't compute the payload ourselves (or they shut it off) do explicit unsigned"

for key := range query {
sort.Strings(query[key])
}
canonQuery := strings.Replace(query.Encode(), "+", "%20", -1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Docs say we need to do sorting after encoding

You must also sort the parameters in the canonical query string alphabetically by key name. The sorting occurs after encoding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's already happening on 134 - Query() turns URL.RawQuery into a map, RawQuery is already in encoded form.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a test that demonstrates this.

"explicit unsigned payload": {
Input: &SignRequestInput{
Request: newRequest(seekable("{}")),
PayloadHash: []byte(v4.UnsignedPayload),
Copy link
Contributor

Choose a reason for hiding this comment

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

customers are expected to set this value always wrapped in bytes. Should we provide this as UnsignedPayloadHash instead so you can do PayloadHash: v4.UnsignedPayloadHash instead of them having to wrap it on a byte?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to expose it as const, if it was []byte it'd have to be var UnsignedPayload = blah.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd still argue that that is more ergonomic, although making it var means people may be able to change it by mistake

Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe this?

func UnsignedPayload() []byte {
    return []byte("UNSIGNED-PAYLOAD")
}

Copy link
Contributor

Choose a reason for hiding this comment

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

That works for me, although since I'm newer to Go I'll let you be the judge of what is more ergonomic for callers


parts := strings.Split(auth, ", ")
if len(parts) != 3 {
err = fmt.Errorf("auth parts have non-3 size %d", len(parts))
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: would be nice to also show these parts for debugging

// 1. creates a bucket in us-east-1
// 2. creates an MRAP that points to that bucket
// 3. polls MRAP status until created
// 4. puts object to MRAP
Copy link
Contributor

Choose a reason for hiding this comment

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

So I suspect is only up to this point that we check sigv4a, right?

Depending on how much "a while" is "a while", I could lean to instead create a bucket if not exists without deleting the bucket at the end, so we skip the lag of MRAP creation. This still validates what we want to validate, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

2-3 minutes in my own testing, it seems.

Copy link
Contributor

Choose a reason for hiding this comment

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

2-3 minutes is fine for more "correctness", but if we ever see this taking longer I wouldn't be opposed to just check if bucket exists and re-using it with a hardcoded name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I did consider adding an env for basically "MRAP_ALIAS" - i.e. if that's set, just bypass the whole setup/teardown and just call against that - but by the time I got this test written I was so done with it that I moved on to other stuff 😂

We can put that in now if you feel strongly that it should be an option.

}

// constant-time byte slice compare
func cmpConst(x, y []byte) (int, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They're not entirely the same. The "breakout" case is c > N-2, ConstantTimeCompare can only tell if equal or not equal.

// AccessKey and SecretKey pair.
//
// Based on FIPS.186-4 Appendix B.4.2
func derivePrivateKey(creds credentials.Credentials) (*ecdsa.PrivateKey, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'll start taking a look at this compared with the docs you referenced, but I'll take time and I'm concerned since these are crypto functions that I'm not familiar with. Looking at what other SDKs do, they basically import the CRT and call it a day, which we don't want to do

We can test correctness by "we were able to call the service and it works" but we may need some extra eyes on this. Curious to hear your thoughts on it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW, this derivation component is ripped from the internal go v2 implementation, which previously had an AppSec review.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But as far as testing correctness goes - yeah, not sure we can do any better than we were doing before in the SDK. In fact we're technically probably testing it better now since we have a dedicated e2e test for it.

func deriveHMACKey(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) {
// verify that we won't overflow the counter
n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size())))
if n > 0x7FFFFFFF {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can we say instead if n > math.MaxInt32

@lucix-aws lucix-aws merged commit f1f22c5 into main Sep 25, 2024
11 checks passed
@lucix-aws lucix-aws deleted the feat-awshttpauth2 branch September 25, 2024 22:45
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.

Support generic SigV4 and SigV4A signers runtime: sigv4a signer runtime: sigv4 signer
3 participants