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 ¶
- Constants
- Variables
- func HTTPBinding(method, path string) string
- type Caller
- type Keyring
- type MemoryNonceStore
- type NonceStore
- type Option
- type Policies
- type Policy
- type SharedSecretKeyring
- type SignatureHeaders
- type Signer
- func (s *Signer) RoundTripper(base http.RoundTripper) http.RoundTripper
- func (s *Signer) Sign(method, path string) (SignatureHeaders, error)
- func (s *Signer) SignRequest(r *http.Request) error
- func (s *Signer) StreamClientInterceptor() grpc.StreamClientInterceptor
- func (s *Signer) UnaryClientInterceptor() grpc.UnaryClientInterceptor
- type SignerOption
- type StaticKeyring
- type Verifier
Examples ¶
Constants ¶
const ( // HeaderSignatureInput is the RFC 9421 signature metadata field. HeaderSignatureInput = "Signature-Input" // HeaderSignature is the RFC 9421 signature value field. HeaderSignature = "Signature" )
Variables ¶
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 ¶
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.
type Keyring ¶
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.
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 ¶
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 ¶
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 ¶
WithKeyring supplies signing secrets. It is required if any binding requires HMAC verification. Nil is ignored.
func WithLogger ¶
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 ¶
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 ¶
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.
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.
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 ¶
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 ¶
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.
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 ¶
New constructs Verifier and validates configuration before serving requests. It performs no network calls.
func (*Verifier) Middleware ¶
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(...)).