aws

package
v1.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

aws — SigV4 signing and credentials for TinyGo

What every AWS service client in this repository needs: SigV4 signing, credentials, environment resolution, and the HTTP client the build selects. storage/s3 and nosql/dynamodb are built on it.

It exists because aws-sdk-go-v2 does not build with TinyGo, so the services here speak their REST APIs directly. Signing is the part they share, and a second copy of it would be a second chance to break the rule that the signature must cover exactly what goes on the wire.

import "github.com/shibukawa/tinygodriver/cloud/aws"

req, _ := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/x-amz-json-1.0")

aws.Sign(req, creds, aws.SignRequest{
	Service:     "dynamodb",
	Region:      "ap-northeast-1",
	PayloadHash: aws.SHA256Hex(payload),
})

Signing another service

The request is an ordinary *http.Request and Sign only reads and sets headers, so a service with no client here is still reachable. Two rules matter.

The service name is not decoration. It enters both the credential scope and the signing key, and the two must agree. A mismatch is a SignatureDoesNotMatch that no local test catches, because both sides of a self-test would use the same wrong value.

S3 is the exception, not the template. SigV4 normalizes and double-encodes the request path for every service except S3, which signs the path exactly as sent. Set DoubleEncodePath for anything with a path other than /:

aws.Sign(req, creds, aws.SignRequest{
	Service: "lambda", Region: region, PayloadHash: hash,
	DoubleEncodePath: true,
})

DynamoDB posts to /, where both rules agree, which is why the flag stays false there.

Presigned URLs

Presign is the same signer with the other output: the credential scope, date, expiry and signed-header list go into the query string as X-Amz-* parameters and the signature is appended as X-Amz-Signature, so the URL alone authorizes the request until it expires. It sets no headers and signs every header on the request, because each signed header is one the eventual sender has to reproduce. The payload hash is normally UnsignedPayload, the body being sent by someone who never sees the credentials.

aws.Presign(req, creds, aws.SignRequest{
	Service: "s3", Region: region, PayloadHash: aws.UnsignedPayload,
}, 15*time.Minute)
url := req.URL.String()

storage/s3 wraps it as Client.Presign.

Credentials

creds := aws.CredentialsFromEnv() // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
creds := aws.Credentials{AccessKeyID: id, SecretAccessKey: secret}

Static values or environment variables. There is no shared credentials file, no SSO, and no metadata service: those need an INI parser, a browser flow, and a link-local HTTP call respectively, none of which belong in a client this size.

RegionFromEnv reads AWS_REGION then AWS_DEFAULT_REGION. EndpointFromEnv(service) reads AWS_ENDPOINT_URL_<SERVICE> then AWS_ENDPOINT_URL, the names the AWS CLI uses.

The HTTP client

NewHTTPClient returns the client a service package uses when the caller supplies none. It selects the transport by build tag and carries the same idle-connection setting to both:

client := aws.NewHTTPClient(aws.ClientOptions{
	Timeout:             10 * time.Second,
	MaxIdleConnsPerHost: 4,
})
Build Transport (aws.Backend)
Standard Go net/http with crypto/tls
TinyGo, or -tags force_tinygo_logic https, TLS through the host OS

DisableRedirectFollowing stops http.Client from following redirects, which a signed request needs: the signature covers the host header, so a redirected request has to be signed again for its new host. storage/s3 does that itself.

What is not here

No credential chain, no retry policy, no endpoint resolution rules, and no request models. Retrying is per-service — DynamoDB must retry throttling, S3 mostly should not — so it lives with the service that knows what its errors mean.

Testing

go test ./cloud/aws/ && go test -tags force_tinygo_logic ./cloud/aws/

The signature tests are known-answer tests. The S3 header case is the example from the SigV4 documentation and the presigned case is the query-parameter example from the S3 documentation; the DynamoDB cases were produced by aws-sdk-go-v2's own signer over the same request and the same header set, which is what makes them evidence rather than a restatement of this implementation.

Documentation

Overview

Package aws holds what every AWS service client in this repository needs: SigV4 signing, credentials, environment resolution, and the HTTP client the build selects.

It exists because the maintained Go SDK does not build with TinyGo — aws-sdk-go-v2 reaches for the full net/http.Transport API through smithy-go, which TinyGo declares as an empty struct — so the services here speak their REST APIs directly. Signing is the part they share, and a second copy of it would be a second chance to break the rule that the signature must cover exactly what goes on the wire.

req, _ := http.NewRequest("POST", endpoint, body)
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
aws.Sign(req, creds, aws.SignRequest{
	Service:     "dynamodb",
	Region:      "ap-northeast-1",
	PayloadHash: aws.SHA256Hex(payload),
})

Presign is the same signer writing its result into the query string instead of the headers, which is what a presigned URL is.

Credentials are static values or environment variables. There is no shared credentials file, no SSO, and no IMDS lookup.

The signer is usable for AWS services this repository has no client for: the request is an ordinary *http.Request, and Sign only reads and sets headers. Two rules matter when doing that. The service name in SignRequest enters both the credential scope and the signing key, so it must be the real one. And for any service with a path other than "/", set DoubleEncodePath: S3 is the exception the other services are not.

Index

Constants

View Source
const (
	// EmptyPayloadHash is SHA-256 of no bytes, sent for bodyless requests.
	EmptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

	// UnsignedPayload signs a request without hashing the body, which is what
	// makes a non-rewindable stream uploadable. S3 accepts it; other services
	// do not, so it is not a general escape hatch.
	UnsignedPayload = "UNSIGNED-PAYLOAD"
)
View Source
const Backend = cloudhttp.Backend

Backend identifies the HTTP stack selected by build constraints.

Variables

View Source
var (
	ErrNoCredentials = errors.New("aws: no credentials configured")
	ErrNoRegion      = errors.New("aws: no region configured")
)

Configuration failures shared by every service client. A rejected signature is a wire response and belongs to the service package that decoded it.

Functions

func CanonicalQuery

func CanonicalQuery(params [][2]string) string

CanonicalQuery renders params sorted and escaped. A parameter with an empty value keeps its "=", which is what S3 subresources such as ?uploads expect.

func CloseIdleConnections

func CloseIdleConnections(client *http.Client)

CloseIdleConnections releases the pooled connections of client, if its transport keeps any. Both https.Transport and net/http.Transport do.

It exists because a service client should be closable without knowing which transport it was given, including one the caller supplied.

func DisableRedirectFollowing

func DisableRedirectFollowing(client *http.Client)

DisableRedirectFollowing stops http.Client from following redirects, so the caller sees them and can sign each hop for its new host.

This file covers every host-Go build, including -tags force_tinygo_logic: the TinyGo code path is exercised there through a standard http.Client, which would otherwise follow redirects that TinyGo's own client never follows.

func EndpointFromEnv

func EndpointFromEnv(service string) string

EndpointFromEnv reads AWS_ENDPOINT_URL_<SERVICE>, then AWS_ENDPOINT_URL, which are the names the AWS CLI uses for pointing a client at a non-AWS endpoint. The service is spelled as in a SigV4 credential scope, so "s3" reads AWS_ENDPOINT_URL_S3 and "dynamodb" reads AWS_ENDPOINT_URL_DYNAMODB.

func NewHTTPClient

func NewHTTPClient(opts ClientOptions) *http.Client

NewHTTPClient builds the default HTTP client for a service package. The idle-connection setting is forwarded to https.Transport on TinyGo builds and to net/http.Transport otherwise, so it means the same thing on both paths.

func Presign added in v1.2.12

func Presign(req *http.Request, creds Credentials, sr SignRequest, expires time.Duration)

Presign signs req through its query string instead of its headers, so the URL alone authorizes the request until expires elapses: a presigned URL, which a browser can GET or PUT without the credentials. The X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders and, with a session token, X-Amz-Security-Token parameters join req.URL.RawQuery in canonical order, and X-Amz-Signature is appended last.

Presign sets no headers. Every header on req is signed instead, because each one is a header the eventual sender has to reproduce exactly, and only the caller knows which ones it will. sr.PayloadHash is normally UnsignedPayload: the body is sent by someone who never sees the credentials, so the signer cannot hash it. expires is rounded up to whole seconds; S3 accepts one second to seven days, and the caller checks that range.

req.URL.RawPath and req.URL.RawQuery must hold the encoded forms produced by URIEncode and CanonicalQuery, as for Sign.

func RegionFromEnv

func RegionFromEnv() string

RegionFromEnv reads AWS_REGION, then AWS_DEFAULT_REGION.

func SHA256Hex

func SHA256Hex(b []byte) string

SHA256Hex returns the hex SHA-256 of b, which is the form a payload hash takes on the wire.

func Sign

func Sign(req *http.Request, creds Credentials, sr SignRequest)

Sign adds x-amz-date, x-amz-content-sha256, the optional session token, and the Authorization header to req.

req.URL.RawPath and req.URL.RawQuery must already hold the encoded forms produced by URIEncode and CanonicalQuery: the canonical request is built from them, so signature and request line cannot drift apart.

func URIEncode

func URIEncode(s string, encodeSlash bool) string

URIEncode escapes s the way SigV4 requires: every byte outside the unreserved set becomes %XX. Path segments keep their separators, query components do not.

For S3 the encoded form is also what goes on the wire, so the signature covers the request line byte for byte.

Types

type ClientOptions

type ClientOptions = cloudhttp.ClientOptions

ClientOptions configures the HTTP client a service package uses when the caller supplies none.

type Credentials

type Credentials struct {
	AccessKeyID     string
	SecretAccessKey string
	SessionToken    string
}

Credentials are the values SigV4 signs with. SessionToken is empty for long-lived keys.

func CredentialsFromEnv

func CredentialsFromEnv() Credentials

CredentialsFromEnv reads AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN. Missing variables yield a zero Credentials, which every client constructor rejects.

There is no shared credentials file, no SSO, and no metadata service: those need an INI parser, a browser flow, and a link-local HTTP call respectively, none of which belong in a client this size.

func (Credentials) Valid

func (c Credentials) Valid() bool

Valid reports whether both required fields are present.

type SignRequest

type SignRequest struct {
	// Service is the name in the credential scope, "s3" or "dynamodb". It also
	// enters the signing key, and the two must agree: a mismatch is a
	// SignatureDoesNotMatch that no local test can catch, because both sides of
	// a self-test would use the same wrong value.
	Service string

	// Region is the signing region, "ap-northeast-1".
	Region string

	// PayloadHash is the hex SHA-256 of the body, EmptyPayloadHash, or
	// UnsignedPayload.
	PayloadHash string

	// DoubleEncodePath selects the canonicalization the SigV4 specification
	// applies to every service except S3, which signs the path exactly as sent.
	// A DynamoDB request posts to "/", where both rules agree, so this stays
	// false there too; a new service with a real path must set it.
	DoubleEncodePath bool

	// Time is the signing time. The zero value means now.
	Time time.Time
}

SignRequest is everything about a signature that is not the request or the credentials.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL