-
Notifications
You must be signed in to change notification settings - Fork 76
support sigv4[a] and non-rule endpoint resolution in generic clients #687
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| module github.com/aws/smithy-go/aws-http-auth-schemes | ||
|
|
||
| go 1.24 | ||
|
|
||
| require ( | ||
| github.com/aws/smithy-go v1.27.3 | ||
| github.com/aws/smithy-go/aws-http-auth v1.2.0 | ||
| ) | ||
|
|
||
| replace github.com/aws/smithy-go => .. | ||
|
|
||
| replace github.com/aws/smithy-go/aws-http-auth => ../aws-http-auth |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Package identity provides the AWS credential identity types shared by the | ||
| // generic smithy-go AWS auth schemes (SigV4, SigV4A). | ||
| package identity | ||
|
|
||
| import ( | ||
| "context" | ||
| "time" | ||
|
|
||
| smithy "github.com/aws/smithy-go" | ||
| "github.com/aws/smithy-go/auth" | ||
| "github.com/aws/smithy-go/aws-http-auth/credentials" | ||
| ) | ||
|
|
||
| // AWSCredentialIdentity is the [auth.Identity] carrying AWS credentials | ||
| // through the auth pipeline. | ||
| type AWSCredentialIdentity struct { | ||
| credentials.Credentials | ||
| } | ||
|
|
||
| var _ auth.Identity = (*AWSCredentialIdentity)(nil) | ||
|
|
||
| // Expiration returns when the underlying credentials expire. | ||
| func (i *AWSCredentialIdentity) Expiration() time.Time { | ||
| return i.Expires | ||
| } | ||
|
|
||
| // AWSCredentialIdentityResolver resolves an [AWSCredentialIdentity]. | ||
| // | ||
| // This is the concrete, credential-flavored counterpart to [auth.IdentityResolver] | ||
| // for the SigV4/SigV4A auth schemes. Implementations resolve AWS credentials | ||
| // (static, environment, assumed-role, etc.) and return them boxed as an | ||
| // [AWSCredentialIdentity]. | ||
| type AWSCredentialIdentityResolver interface { | ||
| GetIdentity(context.Context, smithy.Properties) (*AWSCredentialIdentity, error) | ||
| } | ||
|
|
||
| // staticAWSCredentialIdentityResolver resolves a fixed, unchanging | ||
| // [AWSCredentialIdentity]. | ||
| type staticAWSCredentialIdentityResolver struct { | ||
| identity AWSCredentialIdentity | ||
| } | ||
|
|
||
| var _ AWSCredentialIdentityResolver = (*staticAWSCredentialIdentityResolver)(nil) | ||
|
|
||
| // GetIdentity implements [AWSCredentialIdentityResolver]. | ||
| func (r *staticAWSCredentialIdentityResolver) GetIdentity(context.Context, smithy.Properties) (*AWSCredentialIdentity, error) { | ||
| return &r.identity, nil | ||
| } | ||
|
|
||
| // NewStaticAWSCredentialIdentityResolver returns an [AWSCredentialIdentityResolver] | ||
| // that always resolves to the given, unchanging AWS credentials. | ||
| func NewStaticAWSCredentialIdentityResolver(accessKeyID, secretAccessKey, sessionToken string) AWSCredentialIdentityResolver { | ||
| return &staticAWSCredentialIdentityResolver{ | ||
| identity: AWSCredentialIdentity{ | ||
| Credentials: credentials.Credentials{ | ||
| AccessKeyID: accessKeyID, | ||
| SecretAccessKey: secretAccessKey, | ||
| SessionToken: sessionToken, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // identityResolverAdapter adapts an [AWSCredentialIdentityResolver] to the | ||
| // pipeline's opaque [auth.IdentityResolver] interface. | ||
| type identityResolverAdapter struct { | ||
| Resolver AWSCredentialIdentityResolver | ||
| } | ||
|
|
||
| var _ auth.IdentityResolver = (*identityResolverAdapter)(nil) | ||
|
|
||
| // GetIdentity implements [auth.IdentityResolver]. | ||
| func (a *identityResolverAdapter) GetIdentity(ctx context.Context, props smithy.Properties) (auth.Identity, error) { | ||
| return a.Resolver.GetIdentity(ctx, props) | ||
| } | ||
|
|
||
| // NewIdentityResolver adapts an [AWSCredentialIdentityResolver] to the | ||
| // pipeline's opaque [auth.IdentityResolver] interface. | ||
| func NewIdentityResolver(resolver AWSCredentialIdentityResolver) auth.IdentityResolver { | ||
| return &identityResolverAdapter{Resolver: resolver} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Package payloadhash provides the shared payload hashing logic used by the | ||
| // generic smithy-go AWS auth scheme signers (SigV4, SigV4A). | ||
| package payloadhash | ||
|
|
||
| import ( | ||
| "crypto/sha256" | ||
| "fmt" | ||
| "io" | ||
|
|
||
| smithy "github.com/aws/smithy-go" | ||
| v4 "github.com/aws/smithy-go/aws-http-auth/v4" | ||
| smithyhttp "github.com/aws/smithy-go/transport/http" | ||
| ) | ||
|
|
||
| // Hash returns the SHA256 hash of the request payload, or the SigV4/SigV4A | ||
| // unsigned-payload sentinel if the payload is not signed. | ||
| // | ||
| // The aws-http-auth signers support implicit payload hashing, but in the | ||
| // client pipeline the body is carried on the separate Stream field, so it has | ||
| // to be hashed here instead. | ||
| func Hash(r *smithyhttp.Request, props *smithy.Properties) ([]byte, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So my understanding is this func collapsed/ported all payload hash workflow originated from go v2's sigv4 and runs unsigned payload only when it presents in prop. Can it cover all unsigned cases? For example dynamicPayloadSigningMiddleware applies unsigned payload only for TLS schemed operations like s3.PutObject
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no this is just signing the payload all the time no matter what, which should work for sigv4. the dynamic middleware you're referencing is basically for weird unmodeled AWS behavior |
||
| if unsigned, _ := smithyhttp.GetIsUnsignedPayload(props); unsigned { | ||
| return v4.UnsignedPayload(), nil | ||
| } | ||
|
|
||
| stream := r.GetStream() | ||
| if stream == nil { | ||
| sum := sha256.Sum256(nil) | ||
| return sum[:], nil | ||
| } | ||
|
|
||
| if !r.IsStreamSeekable() { | ||
| return v4.UnsignedPayload(), nil | ||
| } | ||
|
|
||
| h := sha256.New() | ||
| if _, err := io.Copy(h, stream); err != nil { | ||
| return nil, fmt.Errorf("hash payload: %w", err) | ||
| } | ||
| if err := r.RewindStream(); err != nil { | ||
| return nil, fmt.Errorf("rewind payload: %w", err) | ||
| } | ||
|
|
||
| return h.Sum(nil), nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package payloadhash | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "io" | ||
| "testing" | ||
|
|
||
| "github.com/aws/smithy-go" | ||
| smithyhttp "github.com/aws/smithy-go/transport/http" | ||
| ) | ||
|
|
||
| // emptyStringSHA256 is the well-known SHA256 of an empty payload that services compute for | ||
| // bodyless requests (e.g. GET). Sending UNSIGNED-PAYLOAD instead causes InvalidSignatureException. | ||
| const emptyStringSHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | ||
|
|
||
| func TestHashEmptyBody(t *testing.T) { | ||
| r := smithyhttp.NewStackRequest().(*smithyhttp.Request) | ||
|
|
||
| got, err := Hash(r, &smithy.Properties{}) | ||
| if err != nil { | ||
| t.Fatalf("Hash: %v", err) | ||
| } | ||
| if hex.EncodeToString(got) != emptyStringSHA256 { | ||
| t.Fatalf("empty-body hash = %s, want %s", hex.EncodeToString(got), emptyStringSHA256) | ||
| } | ||
| } | ||
|
|
||
| func TestHashSeekableBody(t *testing.T) { | ||
| body := []byte(`{"name":"foo"}`) | ||
| r := smithyhttp.NewStackRequest().(*smithyhttp.Request) | ||
| r, err := r.SetStream(bytes.NewReader(body)) | ||
| if err != nil { | ||
| t.Fatalf("SetStream: %v", err) | ||
| } | ||
|
|
||
| got, err := Hash(r, &smithy.Properties{}) | ||
| if err != nil { | ||
| t.Fatalf("Hash: %v", err) | ||
| } | ||
| want := sha256.Sum256(body) | ||
| if !bytes.Equal(got, want[:]) { | ||
| t.Fatalf("body hash = %x, want %x", got, want) | ||
| } | ||
|
|
||
| // stream must be rewound so the body can still be sent | ||
| rest, err := io.ReadAll(r.GetStream()) | ||
| if err != nil { | ||
| t.Fatalf("read stream after hashing: %v", err) | ||
| } | ||
| if !bytes.Equal(rest, body) { | ||
| t.Fatalf("stream not rewound: got %q, want %q", rest, body) | ||
| } | ||
| } | ||
|
|
||
| func TestHashUnsignedOverride(t *testing.T) { | ||
| r := smithyhttp.NewStackRequest().(*smithyhttp.Request) | ||
| var props smithy.Properties | ||
| smithyhttp.SetIsUnsignedPayload(&props, true) | ||
|
|
||
| got, err := Hash(r, &props) | ||
| if err != nil { | ||
| t.Fatalf("Hash: %v", err) | ||
| } | ||
| if string(got) != "UNSIGNED-PAYLOAD" { | ||
| t.Fatalf("unsigned override = %q, want UNSIGNED-PAYLOAD", got) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package sigv4 | ||
|
|
||
| import ( | ||
| "github.com/aws/smithy-go/auth" | ||
| v4 "github.com/aws/smithy-go/aws-http-auth/v4" | ||
| smithyhttp "github.com/aws/smithy-go/transport/http" | ||
| ) | ||
|
|
||
| // AuthScheme implements request signing for aws.auth#sigv4. | ||
| type AuthScheme struct { | ||
| signer *signer | ||
| } | ||
|
|
||
| var _ smithyhttp.AuthScheme = (*AuthScheme)(nil) | ||
|
|
||
| // NewAuthScheme returns a SigV4 [AuthScheme] backed by a default signer | ||
| // configured with the given options. | ||
| func NewAuthScheme(opts ...v4.SignerOption) *AuthScheme { | ||
| return &AuthScheme{signer: newSigner(opts...)} | ||
| } | ||
|
|
||
| // SchemeID implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) SchemeID() string { | ||
| return auth.SchemeIDSigV4 | ||
| } | ||
|
|
||
| // IdentityResolver implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) IdentityResolver(o auth.IdentityResolverOptions) auth.IdentityResolver { | ||
| return o.GetIdentityResolver(s.SchemeID()) | ||
| } | ||
|
|
||
| // Signer implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) Signer() smithyhttp.Signer { | ||
| return s.signer | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package sigv4 | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/aws/smithy-go" | ||
| "github.com/aws/smithy-go/auth" | ||
| awssigv4 "github.com/aws/smithy-go/aws-http-auth/sigv4" | ||
| v4 "github.com/aws/smithy-go/aws-http-auth/v4" | ||
| "github.com/aws/smithy-go/aws-http-auth-schemes/identity" | ||
| "github.com/aws/smithy-go/aws-http-auth-schemes/internal/payloadhash" | ||
| smithyhttp "github.com/aws/smithy-go/transport/http" | ||
| ) | ||
|
|
||
| // signer adapts the standalone [awssigv4.Signer] to the client-side | ||
| // [smithyhttp.Signer] interface, sourcing the signing name and region from the | ||
| // request's resolved auth properties. | ||
| type signer struct { | ||
| Signer *awssigv4.Signer | ||
| } | ||
|
|
||
| var _ smithyhttp.Signer = (*signer)(nil) | ||
|
|
||
| // newSigner returns a signer backed by a default SigV4 signer configured | ||
| // with the given options. | ||
| func newSigner(opts ...v4.SignerOption) *signer { | ||
| return &signer{Signer: awssigv4.New(opts...)} | ||
| } | ||
|
|
||
| // SignRequest implements [smithyhttp.Signer]. | ||
| func (a *signer) SignRequest( | ||
| ctx context.Context, r *smithyhttp.Request, ident auth.Identity, props smithy.Properties, | ||
| ) error { | ||
| ci, ok := ident.(*identity.AWSCredentialIdentity) | ||
| if !ok { | ||
| return fmt.Errorf("sigv4: unexpected identity type %T", ident) | ||
| } | ||
|
|
||
| name, ok := smithyhttp.GetSigV4SigningName(&props) | ||
| if !ok { | ||
| return fmt.Errorf("sigv4: signing name is required") | ||
| } | ||
|
|
||
| region, ok := smithyhttp.GetSigV4SigningRegion(&props) | ||
| if !ok { | ||
| return fmt.Errorf("sigv4: signing region is required") | ||
| } | ||
|
|
||
| hash, err := payloadhash.Hash(r, &props) | ||
| if err != nil { | ||
| return fmt.Errorf("sigv4: %w", err) | ||
| } | ||
|
|
||
| in := &awssigv4.SignRequestInput{ | ||
| Request: r.Request, | ||
| Credentials: ci.Credentials, | ||
| Service: name, | ||
| Region: region, | ||
| PayloadHash: hash, | ||
| } | ||
|
|
||
| if err := a.Signer.SignRequest(in); err != nil { | ||
| return fmt.Errorf("sigv4: sign request: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package sigv4a | ||
|
|
||
| import ( | ||
| "github.com/aws/smithy-go/auth" | ||
| v4 "github.com/aws/smithy-go/aws-http-auth/v4" | ||
| smithyhttp "github.com/aws/smithy-go/transport/http" | ||
| ) | ||
|
|
||
| // AuthScheme implements request signing for aws.auth#sigv4a. | ||
| type AuthScheme struct { | ||
| signer *signer | ||
| } | ||
|
|
||
| var _ smithyhttp.AuthScheme = (*AuthScheme)(nil) | ||
|
|
||
| // NewAuthScheme returns a SigV4A [AuthScheme] backed by a default signer | ||
| // configured with the given options. | ||
| func NewAuthScheme(opts ...v4.SignerOption) *AuthScheme { | ||
| return &AuthScheme{signer: newSigner(opts...)} | ||
| } | ||
|
|
||
| // SchemeID implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) SchemeID() string { | ||
| return auth.SchemeIDSigV4A | ||
| } | ||
|
|
||
| // IdentityResolver implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) IdentityResolver(o auth.IdentityResolverOptions) auth.IdentityResolver { | ||
| return o.GetIdentityResolver(s.SchemeID()) | ||
| } | ||
|
|
||
| // Signer implements [smithyhttp.AuthScheme]. | ||
| func (s *AuthScheme) Signer() smithyhttp.Signer { | ||
| return s.signer | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Package sigv4a provides the generic smithy-go client wiring for AWS | ||
| // Signature Version 4a (asymmetric) request signing. | ||
| // | ||
| // It adapts the standalone signer in | ||
| // [github.com/aws/smithy-go/aws-http-auth/sigv4a] to the client auth scheme | ||
| // interfaces in [github.com/aws/smithy-go/transport/http], allowing non-SDK | ||
| // smithy-go clients to sign requests with SigV4A without depending on the AWS | ||
| // SDK for Go. It is a sibling package to | ||
| // [github.com/aws/smithy-go/aws-http-auth-schemes/sigv4] within the same | ||
| // module, so that the core smithy-go runtime does not take on an | ||
| // AWS-specific dependency. | ||
| package sigv4a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure where to put this nit question: with support of sigv4(a) and non-rule ep resolution for user's own client, should we concern min go version in smithy go like go v2 now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two things wouldn't matter for that, but also i thought we've been enforcing the same minimum version in smithy-go that we have been in the sdk