aws-http-auth/sigv4a/sigv4a.go (79 lines of code) (raw):

// Package sigv4a implements request signing for AWS Signature Version 4a // (asymmetric). // // The algorithm for Signature Version 4a is identical to that of plain v4 // apart from the following: // - A request can be signed for multiple regions. This is represented in the // signature using the X-Amz-Region-Set header. The credential scope string // used in the calculation correspondingly lacks the region component from // that of plain v4. // - The string-to-sign component of the calculation is instead signed with // an ECDSA private key. This private key is typically derived from your // regular AWS credentials. package sigv4a import ( "crypto" "crypto/ecdsa" "crypto/rand" "encoding/hex" "net/http" "strings" "time" "github.com/aws/smithy-go/aws-http-auth/credentials" v4internal "github.com/aws/smithy-go/aws-http-auth/internal/v4" v4 "github.com/aws/smithy-go/aws-http-auth/v4" ) const algorithm = "AWS4-ECDSA-P256-SHA256" // Signer signs requests with AWS Signature Version 4a. // // Unlike Sigv4, AWS SigV4a signs requests with an ECDSA private key. This is // derived automatically from the AWS credential identity passed to // SignRequest. This derivation result is cached on the Signer and is uniquely // identified by the access key ID (AKID) of the credentials that were // provided. // // Because of this, the caller is encouraged to create multiple instances of // Signer for different underlying identities (e.g. IAM roles). type Signer struct { options v4.SignerOptions // derived asymmetric credentials privCache *ecdsaCache } // New returns an instance of Signer with applied options. func New(opts ...v4.SignerOption) *Signer { options := v4.SignerOptions{} for _, opt := range opts { opt(&options) } return &Signer{ options: options, privCache: &ecdsaCache{}, } } // SignRequestInput is the set of inputs for the Sigv4a signing process. type SignRequestInput struct { // The input request, which will modified in-place during signing. Request *http.Request // The SHA256 hash of the input request body. // // This value is NOT required to sign the request, but it is recommended to // provide it (or provide a Body on the HTTP request that implements // io.Seeker such that the signer can calculate it for you). Many services // do not accept requests with unsigned payloads. // // If a value is not provided, and DisableImplicitPayloadHashing has not // been set on SignerOptions, the signer will attempt to derive the payload // hash itself. The request's Body MUST implement io.Seeker in order to do // this, if it does not, the magic value for unsigned payload is used. If // the body does implement io.Seeker, but a call to Seek returns an error, // the signer will forward that error. PayloadHash []byte // The identity used to sign the request. Credentials credentials.Credentials // The service for which this request is to be signed. // // The appropriate value for this field is determined by the service // vendor. Service string // The set of regions for which this request is to be signed. // // The sentinel {"*"} is used to indicate that the signed request is valid // in all regions. Callers MUST set a value for this field - the API will // not fill in a default and the resulting signature will ultimately be // invalid. // // The acceptable values for list entries of this field are determined by // the service vendor. RegionSet []string // Wall-clock time used for calculating the signature. // // If the zero-value is given (generally by the caller not setting it), the // signer will instead use the current system clock time for the signature. Time time.Time } // SignRequest signs an HTTP request with AWS Signature Version 4, modifying // the request in-place by adding the headers that constitute the signature. // // SignRequest will modify the request by setting the following headers: // - Host: required in general for HTTP/1.1 as well as for v4-signed requests // - X-Amz-Date: required for v4a-signed requests // - X-Amz-Region-Set: used to convey the regions for which the request is // signed in v4a // - X-Amz-Security-Token: required for v4a-signed requests IF present on // credentials used to sign, otherwise this header will not be set // - Authorization: contains the v4a signature string // // The request MUST have a Host value set at the time that this API is called, // such that it can be included in the signature calculation. Standard library // HTTP clients set this as a request header by default, meaning that a request // signed without a Host value will end up transmitting with the Host header // anyway, which will cause the request to be rejected by the service due to // signature mismatch (the Host header is required to be signed with Sigv4). // // Generally speaking, using http.NewRequest will ensure that request instances // are sufficiently initialized to be used with this API, though it is not // strictly required. // // SignRequest may be called any number of times on an http.Request instance, // the header values set as part of the signature will simply be re-calculated. // Note that v4a signatures are non-deterministic due to the random component // of ECDSA signing, callers should not expect two calls to SignRequest() to // produce an identical signature. func (s *Signer) SignRequest(in *SignRequestInput, opts ...v4.SignerOption) error { options := s.options for _, fn := range opts { fn(&options) } priv, err := s.privCache.Derive(in.Credentials) if err != nil { return err } in.Request.Header.Set("X-Amz-Region-Set", strings.Join(in.RegionSet, ",")) tm := v4internal.ResolveTime(in.Time) signer := &v4internal.Signer{ Request: in.Request, PayloadHash: in.PayloadHash, Time: tm, Credentials: in.Credentials, Options: options, Algorithm: algorithm, CredentialScope: scope(tm, in.Service), Finalizer: &finalizer{priv}, } if err := signer.Do(); err != nil { return err } return nil } func scope(tm time.Time, service string) string { return strings.Join([]string{ tm.Format(v4internal.ShortTimeFormat), service, "aws4_request", }, "/") } type finalizer struct { Secret *ecdsa.PrivateKey } func (f *finalizer) SignString(strToSign string) (string, error) { sig, err := f.Secret.Sign(rand.Reader, v4internal.Stosha(strToSign), crypto.SHA256) if err != nil { return "", err } return hex.EncodeToString(sig), nil }