Skip to content

Signer

Signer adds AWS Signature Version 4 authentication to HTTP requests. Import github.com/rambow-cloud/powertools-lambda-go/signer. Standalone signing does not send a request; HTTPClient provides a signed transport.

See installation and the compatibility baseline.

Complete example

Run the complete example with go run ./examples/signing and CGO_ENABLED=0. It uses synthetic credentials and signs API Gateway, Lambda Function URL and AppSync requests without sending them.

// Command signing demonstrates standalone signatures with synthetic credentials.
// It sends no requests and prints no credentials or signature values.
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/rambow-cloud/powertools-lambda-go/signer"
)

func main() {
    credentials := aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
        return aws.Credentials{AccessKeyID: "EXAMPLE", SecretAccessKey: "example-only"}, nil
    })
    for _, target := range []struct{ service, address string }{
        {"execute-api", "https://example.execute-api.ap-east-1.amazonaws.com/orders"},
        {"lambda", "https://example.lambda-url.ap-east-1.on.aws/"},
        {"appsync", "https://example.appsync-api.ap-east-1.amazonaws.com/graphql"},
    } {
        s, err := signer.New(signer.Config{Service: target.service, Region: "ap-east-1", Credentials: credentials})
        if err != nil {
            log.Fatal(err)
        }
        request, err := http.NewRequest(http.MethodGet, target.address, nil)
        if err != nil {
            log.Fatal(err)
        }
        signed, err := s.Sign(request)
        if err != nil {
            log.Fatal(err)
        }
        _ = signed.Body.Close()
        fmt.Println(target.service, signed.Header.Get("Authorization") != "")
    }
}

Input and output

Stdout is exactly the following. true means the signed copy contains an Authorization header. These lines are ordinary program output, not structured Logger records. The example prints neither credentials nor signatures and does not establish service-side authorization.

execute-api true
lambda true
appsync true

Objects and lifecycle

Object Responsibility
s Reusable service, region, clock and credentials-provider configuration.
request / signed Sign returns a signed copy; the caller owns body closure and request replay rules.
HTTPClient Copied client with a signing transport; credentials, retries and redirects stay explicit.

TypeScript feature coverage

Compared with the official v2.35.0 signer guide and the pinned npm implementation. The table maps capabilities; it does not certify every native type or service behavior.

TypeScript feature Go API or approach Compatibility scope
Signed fetch / other clients Sign, Transport, HTTPClient Go request/transport interfaces replace fetch.
Region / credentials Config.Region, Credentials Environment or injected SDK provider; no automatic config-loader network calls.
Errors ConfigError, SigningError Unwrap causes/cancellation; unsigned request is not sent on failure.
Bodies / redirects Replayable signed copies and client policy Ownership and trusted redirect policy differ from JavaScript Request.

Executable evidence: signer/signer_test.go. See the verification scope and project progress for open gates.

Configuration and errors

Region defaults to the raw AWS_REGION value. Service and resolved region must be nonempty. The default provider reads AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN for each signing operation, allowing environment credential refresh. It makes no credential-discovery network requests. Supply any aws.CredentialsProvider for static credentials, role assumption, profiles, or caching; callers own that provider and its I/O. aws.CredentialsProviderFunc and aws.NewCredentialsCache can be used directly.

Clock supports deterministic signing tests. The constructor returns *ConfigError for missing configuration, and the default provider returns it for absent credential variables. Injected provider errors propagate unchanged. Body/request/signing failures use *SigningError with Unwrap; context cancellation remains discoverable with errors.Is.

Ownership, redirects, and composition

Sign(*http.Request) signs without sending. It clones the URL and headers and returns a replayable body with GetBody. If the input supplies GetBody, its body stream is untouched. Otherwise the finite stream is buffered and restored on the original request, preserving its Close. After a read failure the successfully read prefix is restored in front of the unread stream. Standalone callers close both bodies and must not use the input concurrently with signing. Buffering is proportional to payload size; context checks cannot forcibly interrupt an arbitrary blocking application Reader.

Transport accepts any Signer interface and any http.RoundTripper. It owns and closes the incoming body even if signing fails, while the downstream transport owns the signed body. It performs no retry loop. HTTPClient copies its supplied client and defaults to stopping at redirects. Supply a deliberate CheckRedirect policy to allow trusted destinations; each permitted redirect is signed again, and normal Go replay rules apply to 307/308 responses.

Compose with tracing as signer.HTTPClient(s, tr.HTTPClient(baseClient)). Neither utility imports the other. AWS SDK service clients already sign their own requests and must not receive another signing layer.

Reference scope and intentional differences

Reference: TypeScript v2.35.0. Thirteen fixed-time cases cover GET/JSON/binary/empty bodies, escaped paths, ports, repeated/whitespace headers, session tokens, explicit unsigned payloads, encoded queries, and service names. Twelve signatures match exactly. The duplicate-query case records an intentional difference: the upstream request converter signs only the last occurrence while returning the original URL. Go signs every occurrence, preserving the original URL and a valid canonical query.

Other Go contracts are explicit: empty Region means environment fallback; missing service is rejected; malformed query encoding and inconsistent Content-Length are rejected. Redirects require an explicit policy. Signer uses Go request-body ownership, and Go transport-generated headers are not signed unless supplied explicitly. Custom Host values and service-specific URL/path normalization follow the Go SDK; these do not establish every web Request conversion edge case. General presigning and SigV4a are not part of the pinned Powertools Signer API and are not implemented here.

Tests cover body replay/errors, configuration refresh, cancellation, custom transports, redirects, and concurrent use. Local Docker verifies synthetic signatures and OTel composition. These checks do not establish live IAM authorization, service-side acceptance for every path, or performance budgets.

Sources