Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions aws-http-auth-schemes/go.mod
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
81 changes: 81 additions & 0 deletions aws-http-auth-schemes/identity/identity.go
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 {

Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Collaborator Author

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

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}
}
45 changes: 45 additions & 0 deletions aws-http-auth-schemes/internal/payloadhash/payloadhash.go
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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
}
69 changes: 69 additions & 0 deletions aws-http-auth-schemes/internal/payloadhash/payloadhash_test.go
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)
}
}
35 changes: 35 additions & 0 deletions aws-http-auth-schemes/sigv4/auth_scheme.go
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
}
68 changes: 68 additions & 0 deletions aws-http-auth-schemes/sigv4/signer.go
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
}
35 changes: 35 additions & 0 deletions aws-http-auth-schemes/sigv4a/auth_scheme.go
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
}
12 changes: 12 additions & 0 deletions aws-http-auth-schemes/sigv4a/doc.go
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
Loading
Loading