hmac

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 17 Imported by: 0

README

hmac

Русский перевод · Module guide

HTTP Message Signatures using standard RFC 9421 Signature-Input and Signature fields and hmac-sha256. HTTP middleware and gRPC interceptors share the same verification core.

Supported components and parameters

The package supports request signatures covering exactly @method and @path, in either order. gRPC uses method POST and its full RPC path. HTTP uses the case-sensitive method and percent-encoded path, preserving escapes. Query, authority, headers, body, responses, other algorithms, and signatures with other covered-component sets are not supported.

created (integer) and keyid (nonempty string) are required. alg, when supplied, must be hmac-sha256; otherwise the verifier's configured algorithm applies. expires, when supplied, is checked. nonce is required when replay protection is enabled. Standard parameter types are validated; a standard string tag can be included. Other signature metadata participates in canonical serialization but has no additional local policy meaning.

The signer emits one sig1 signature with created, keyid, alg, and a cryptographically random nonce. The verifier accepts multiple labels and verifies supported candidates independently. It rejects ambiguous duplicate labels and mismatched label sets. Structured Fields parsing, string escaping, and canonical serialization use github.com/dunglas/httpsfv; MAC generation/comparison use Go's crypto/hmac.

Method/path signatures do not authenticate request content or bind a request to a host. Use TLS, distinct service keys, and application authorization. Do not use these signatures as end-to-end body integrity across intermediaries that can change unsigned content.

HTTP example

verifier, err := hmac.New(
    hmac.WithKeyring(hmac.StaticKeyring{"example-client": secret}),
    hmac.WithNonceStore(hmac.NewMemoryNonceStore()),
)
if err != nil {
    return err
}
handler := verifier.Middleware(applicationHandler)
signer, err := hmac.NewSigner("example-client", secret)
if err != nil {
    return err
}
client := &http.Client{Transport: signer.RoundTripper(http.DefaultTransport)}

secret must come from application secret management; applicationHandler supplies routes. The executable TLS example makes a signed request and reads the authenticated Caller.KeyID in a real handler.

Signer.Sign(method, encodedPath) returns field values. Signer.SignRequest(r) sets/replaces fields after method and URL are finalized. RoundTripper(base) clones requests before signing, preserves the caller's headers, and closes the body if signing fails. A nil base uses http.DefaultTransport.

Verification failure returns HTTP 403 with a generic body. The response does not include a WWW-Authenticate challenge. HTTPBinding(method, encodedPath) defines exact route policies; method case and encoded path matter.

gRPC integration

serverOptions := []grpc.ServerOption{
    grpc.ChainUnaryInterceptor(verifier.UnaryServerInterceptor()),
    grpc.ChainStreamInterceptor(verifier.StreamServerInterceptor()),
}
clientOptions := []grpc.DialOption{
    grpc.WithChainUnaryInterceptor(signer.UnaryClientInterceptor()),
    grpc.WithChainStreamInterceptor(signer.StreamClientInterceptor()),
}

Pass these options to grpc-go or the chassis adapters alongside explicit TLS credentials. Outgoing interceptors replace existing signature metadata and preserve unrelated metadata. Incoming failures return Unauthenticated. Stream signatures authenticate stream opening, not individual messages. Retrying a request must produce a new signature/nonce; do not reuse a signed context with replay protection.

Policies, keys, and replay

Required() is the default and the zero-value Policy. Skip() accepts unsigned calls without adding a caller. WithPolicies(Policies{binding: policy}) merges overrides. Health/reflection methods use the same policies; configure unsigned access explicitly when needed. With Skip() as the default and no required overrides, a keyring is unnecessary.

StaticKeyring maps key IDs to secrets. Configure it before serving and treat the map and byte slices as immutable; use an application-owned synchronized Keyring for live rotation. SharedSecretKeyring authenticates possession of one shared secret, not distinct caller identities: any holder can choose any nonempty key ID. Use separate secrets when identity matters. Empty secrets are rejected. NewSigner copies its secret.

Without a NonceStore, a captured signature can be replayed throughout its timestamp window. MemoryNonceStore (also usable at zero value) atomically records nonces for one process, scoped internally to key ID. A nonce is stored only after successful MAC validation and retained through the remaining accepted lifetime. Lazy cleanup avoids a background worker. Multiple replicas or restarts need an application-provided shared store. Shared stores must perform atomic check-and-insert and fail closed on storage failure using the interface's boolean result.

Option Default / constraint
WithKeyring Required if any binding requires signatures; nil ignored
WithDefaultPolicy Required()
WithPolicies Exact gRPC method or HTTPBinding keys; later entries win
WithClockSkew ±5 minutes; nonpositive ignored
WithNonceStore Disabled by default; nil ignored
WithLogger slog.Default(); nil ignored
Signer WithClock / WithNonceFunc time.Now / 128 random bits; replace only for controlled testing

FromContext(ctx) returns (Caller, bool). Caller contains KeyID; authorization beyond key possession remains application-owned. ErrMissingSignature, ErrUnknownKey, ErrStaleTimestamp, ErrBadSignature, and ErrReplay describe internal verification failures; adapters expose generic failures.

Tests and references

Tests and protocol tests cover HTTP/gRPC/stream adapters, policy defaults, escaping, signature labels, timestamp overflow/expiry, key/nonce isolation, concurrent replay detection, and an RFC 4231 HMAC vector. FuzzSignatureFields exercises malformed fields. See RFC 9421, Structured Fields RFC 9651, and RFC 4231.

Documentation

Overview

Package hmac authenticates service callers using HMAC request signatures for gRPC and HTTP.

A signature proves possession of a service secret. Separate keys distinguish service callers; a shared key authenticates only membership in the group of secret holders.

The verification core is transport independent. Thin adapters expose gRPC server/client interceptors and HTTP middleware/signing. Policy bindings are full gRPC method names or HTTPBinding(method, path).

Required is the default policy; Skip permits unsigned requests. Method or route policies override the default. Health and reflection require explicit policies, just like application methods.

The signature covers the method, path, key ID, creation time, and nonce. Request bodies are not covered by this profile. Use TLS for transport confidentiality and integrity. Timestamp validation limits signature age; a NonceStore additionally rejects replay within the validity window.

Example:

verifier, err := hmac.New(
    hmac.WithKeyring(hmac.StaticKeyring{
        "wallet-service": []byte(cfg.HMAC.WalletSecret),
    }),
    hmac.WithPolicies(hmac.Policies{
        pb.MyService_Health_FullMethodName: hmac.Skip(),
    }),
)
...
grpcserver.New(addr, grpcserver.WithServerOptions(
    grpc.ChainUnaryInterceptor(verifier.UnaryServerInterceptor()),
    grpc.ChainStreamInterceptor(verifier.StreamServerInterceptor()),
))

For HTTP, wrap the application handler:

mux := http.NewServeMux()
// Register application routes.
srv := httpserver.New(addr, verifier.Middleware(mux))

Handlers retrieve the caller from the context:

caller, ok := hmac.FromContext(ctx)

Index

Examples

Constants

View Source
const (
	// HeaderSignatureInput is the RFC 9421 signature metadata field.
	HeaderSignatureInput = "Signature-Input"
	// HeaderSignature is the RFC 9421 signature value field.
	HeaderSignature = "Signature"
)

Variables

View Source
var (
	// ErrMissingSignature indicates incomplete signature fields.
	ErrMissingSignature = errors.New("hmac: missing signature")
	// ErrUnknownKey indicates that the keyring does not contain the key ID.
	ErrUnknownKey = errors.New("hmac: unknown key id")
	// ErrStaleTimestamp indicates a creation time outside the accepted clock window.
	ErrStaleTimestamp = errors.New("hmac: timestamp out of range")
	// ErrBadSignature indicates a signature mismatch.
	ErrBadSignature = errors.New("hmac: invalid signature")
	// ErrReplay indicates a nonce already used within the validity window.
	ErrReplay = errors.New("hmac: nonce replay detected")
)

Verification errors are mapped to protocol-specific responses by the adapters: gRPC Unauthenticated or HTTP 403. Clients receive a generic message while detailed diagnostics stay in service logs.

Functions

func HTTPBinding

func HTTPBinding(method, path string) string

HTTPBinding forms a route policy key. HTTP methods are case-sensitive under RFC 9110, so their case is never changed during signing or policy matching.

Types

type Caller

type Caller struct {
	// KeyID is the signing key identifier, conventionally the caller's service name.
	KeyID string
}

Caller identifies the service authenticated by the request signature. Handlers retrieve it with FromContext to make caller-specific access decisions.

func FromContext

func FromContext(ctx context.Context) (Caller, bool)

FromContext returns the authenticated caller. ok is false when signature verification was skipped or the HMAC adapter was not installed.

type Keyring

type Keyring interface {
	Secret(keyID string) (secret []byte, ok bool)
}

Keyring resolves signing secrets by key ID. The interface lets applications choose storage and rotation independently of this package. An unknown key returns ok=false and causes ErrUnknownKey.

type MemoryNonceStore

type MemoryNonceStore struct {
	// contains filtered or unexported fields
}

MemoryNonceStore is a concurrency-safe nonce store for one process. Multiple replicas require an application-supplied shared store; a replay sent to another replica is not detected by independent memory stores.

func NewMemoryNonceStore

func NewMemoryNonceStore() *MemoryNonceStore

NewMemoryNonceStore creates an empty in-memory nonce store.

func (*MemoryNonceStore) Seen

func (m *MemoryNonceStore) Seen(_ context.Context, nonce string, ttl time.Duration) bool

Seen implements NonceStore. Expired entries are cleaned lazily, avoiding a background goroutine that would need explicit shutdown.

type NonceStore

type NonceStore interface {
	// Seen atomically registers a nonce and reports whether it was already used.
	// A true result rejects the request as a replay. ttl is the retention period,
	// covering the full accepted signature lifetime.
	Seen(ctx context.Context, nonce string, ttl time.Duration) bool
}

NonceStore records used nonces to prevent replay within the accepted timestamp window.

type Option

type Option func(*Verifier)

Option configures Verifier using functional options.

func WithClockSkew

func WithClockSkew(d time.Duration) Option

WithClockSkew sets the accepted timestamp difference in either direction. A smaller window limits replay time but tolerates less clock drift. Nonpositive values preserve the default five minutes.

func WithDefaultPolicy

func WithDefaultPolicy(p Policy) Option

WithDefaultPolicy configures bindings without explicit policies.

WithDefaultPolicy(hmac.Required()) // Require signatures by default.
WithDefaultPolicy(hmac.Skip())     // Require only explicit overrides.

The default is Required.

func WithKeyring

func WithKeyring(k Keyring) Option

WithKeyring supplies signing secrets. It is required if any binding requires HMAC verification. Nil is ignored.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the component logger. Nil preserves the existing logger.

func WithNonceStore

func WithNonceStore(s NonceStore) Option

WithNonceStore enables replay protection: a nonce can be accepted only once within its validity window. Without a store, timestamps limit signature age but captured requests can be replayed until expiration. Nil is ignored.

MemoryNonceStore protects one process. Multiple replicas require an application-supplied shared implementation of NonceStore.

func WithPolicies

func WithPolicies(policies Policies) Option

WithPolicies merges per-binding policy overrides; later entries win.

// Require HMAC except for Health.
WithPolicies(hmac.Policies{pb.Svc_Health_FullMethodName: hmac.Skip()})

// With a Skip default, require HMAC only for this method.
WithPolicies(hmac.Policies{pb.Svc_Settle_FullMethodName: hmac.Required()})

// Configure an HTTP route.
WithPolicies(hmac.Policies{hmac.HTTPBinding("POST", "/v1/settle"): hmac.Required()})

type Policies

type Policies map[string]Policy

Policies maps full gRPC method names or HTTPBinding(method, path) values to signature policies.

type Policy

type Policy struct {
	// contains filtered or unexported fields
}

Policy specifies whether a gRPC method or HTTP route requires HMAC. Use Required or Skip; the zero value requires verification.

func Required

func Required() Policy

Required requires a valid signature from a known key within the timestamp window, plus an unused nonce when replay protection is enabled.

func Skip

func Skip() Policy

Skip disables signature verification, for example on public health endpoints or routes protected by another mechanism.

type SharedSecretKeyring

type SharedSecretKeyring []byte

SharedSecretKeyring uses one secret for every nonempty key ID.

It simplifies configuration but cannot authenticate distinct caller identities: any holder of the shared secret can claim any KeyID. Use StaticKeyring with separate secrets when caller identity matters.

An empty secret is treated as absent.

func (SharedSecretKeyring) Secret

func (k SharedSecretKeyring) Secret(keyID string) ([]byte, bool)

Secret returns the shared secret for a nonempty key ID.

type SignatureHeaders

type SignatureHeaders struct {
	// Input is the RFC 9421 Signature-Input structured-field dictionary
	// (covered components plus created, keyid, alg, nonce).
	Input string
	// Signature is the RFC 9421 Signature structured-field dictionary
	// (HMAC-SHA256 over the signature base).
	Signature string
}

SignatureHeaders holds the outgoing Signature-Input and Signature values.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer holds the client key ID and secret and produces signature fields. Transport adapters attach them to HTTP headers or gRPC metadata.

func NewSigner

func NewSigner(keyID string, secret []byte, opts ...SignerOption) (*Signer, error)

NewSigner configures a signing client. keyID identifies the key shared with the verifier. Empty key IDs and secrets are rejected at construction.

func (*Signer) RoundTripper

func (s *Signer) RoundTripper(base http.RoundTripper) http.RoundTripper

RoundTripper signs each outgoing request through the wrapped transport.

client := &http.Client{Transport: signer.RoundTripper(http.DefaultTransport)}

A nil base uses http.DefaultTransport. Requests are cloned before adding headers so the caller's request is not mutated.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/platfordotme/chassis/hmac"
)

func main() {
	// Demonstration secret only. Load independent random secrets from the
	// application's secret manager in a deployed service.
	secret := []byte("example-only-secret-do-not-deploy")
	verifier, err := hmac.New(
		hmac.WithKeyring(hmac.StaticKeyring{"example-client": secret}),
		hmac.WithNonceStore(hmac.NewMemoryNonceStore()),
	)
	if err != nil {
		panic(err)
	}
	server := httptest.NewTLSServer(verifier.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		caller, _ := hmac.FromContext(r.Context())
		fmt.Fprintln(w, caller.KeyID)
	})))
	defer server.Close()
	signer, err := hmac.NewSigner("example-client", secret)
	if err != nil {
		panic(err)
	}
	client := server.Client()
	client.Transport = signer.RoundTripper(client.Transport)
	response, err := client.Get(server.URL + "/resource")
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)
	if err != nil {
		panic(err)
	}
	fmt.Print(string(body))
}
Output:
example-client

func (*Signer) Sign

func (s *Signer) Sign(method, path string) (SignatureHeaders, error)

Sign signs the supplied HTTP method and path. For gRPC, use POST and the full method name. Nonce generation failures are returned to the caller.

func (*Signer) SignRequest

func (s *Signer) SignRequest(r *http.Request) error

SignRequest sets signature headers after the request method and URL have been finalized. The profile does not sign the request body.

func (*Signer) StreamClientInterceptor

func (s *Signer) StreamClientInterceptor() grpc.StreamClientInterceptor

StreamClientInterceptor signs an outgoing stream once, when it opens.

func (*Signer) UnaryClientInterceptor

func (s *Signer) UnaryClientInterceptor() grpc.UnaryClientInterceptor

UnaryClientInterceptor signs outgoing unary calls and attaches signature metadata. Install it with grpc.WithChainUnaryInterceptor.

type SignerOption

type SignerOption func(*Signer)

SignerOption configures Signer.

func WithClock

func WithClock(now func() time.Time) SignerOption

WithClock replaces the signature clock, primarily for deterministic tests. Nil is ignored.

func WithNonceFunc

func WithNonceFunc(fn func() (string, error)) SignerOption

WithNonceFunc replaces the nonce generator, primarily for deterministic tests. Nil is ignored.

type StaticKeyring

type StaticKeyring map[string][]byte

StaticKeyring is an immutable in-memory secret map. During key rotation, retain both signing keys until all callers use the replacement key. Empty secrets are treated as absent so missing configuration cannot become a usable empty signing key.

func (StaticKeyring) Secret

func (k StaticKeyring) Secret(keyID string) ([]byte, bool)

Secret implements Keyring.

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier holds signature verification settings: keys, policies, clock skew, and an optional nonce store. Construct it once and install its adapters.

func New

func New(opts ...Option) (*Verifier, error)

New constructs Verifier and validates configuration before serving requests. It performs no network calls.

func (*Verifier) Middleware

func (v *Verifier) Middleware(next http.Handler) http.Handler

Middleware verifies HTTP requests according to HTTPBinding(method, path). Use the same binding for WithPolicies entries.

srv := httpserver.New(addr, verifier.Middleware(mux))

Skip leaves requests unchanged. Verification failures return HTTP 403 with a generic body and log details internally.

func (*Verifier) StreamServerInterceptor

func (v *Verifier) StreamServerInterceptor() grpc.StreamServerInterceptor

StreamServerInterceptor verifies streaming calls and passes Caller through a stream wrapper that replaces the context.

func (*Verifier) UnaryServerInterceptor

func (v *Verifier) UnaryServerInterceptor() grpc.UnaryServerInterceptor

UnaryServerInterceptor verifies request signatures according to method policy. Install it through grpcserver.WithServerOptions(grpc.ChainUnaryInterceptor(...)).

Jump to

Keyboard shortcuts

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