The docs for AWS Signature Version 4 are spread across multiple pages. I’m considering writing a library for it, so I’m making a summary here.

First, there is a canonical request which is just a string that includes the data that must not change between when the signature is generated and AWS or a compatible API like DigitalOcean Spaces receives the request. Note that SignedHeaders is just the headers that will be signed. When computing this string, they aren’t signed:

CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))

But that isn’t all the data. The metadata for the request must also be signed – especially the time which will help prevent a request from being reused inappropriately if intercepted in transit (normally AWS APIs only accept requests within five minutes of the time):

StringToSign =
    Algorithm + \n +
    RequestDateTime + \n +
    CredentialScope + \n +
    HashedCanonicalRequest

A key for signing the request needs to be made. This opens up the possibility of having a component sign requests without giving it access to the original keys, and being restricted to a service, region, and date (the time isn’t included). It might be to make it harder to use brute force to get the original keys. Extra hoops can help make something more secure, and because you only need to implement it once, it’s often worth the extra code.

kSecret = your secret access key
kDate = HMAC("AWS4" + kSecret, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")

The signature is obtained:

signature = HexEncode(HMAC(derived signing key, string to sign))

Finally it’s added as an Authorization header:

Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature

There are a bunch of test cases available that can be downloaded.