hmacauth

package
v0.14.0 Latest Latest
Warning

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

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

Documentation

Overview

Package hmacauth provides HMAC-SHA256 signing and verification for request authentication between Fred components (Fred ↔ backends).

Wire format

Header: "X-Fred-Signature: t=<unix-timestamp>,sha256=<hex-encoded-hmac>"

Signed canonical string

The HMAC is computed over a four-field canonical string:

"<timestamp>\n<METHOD>\n<canonical-URI>\n<hex(sha256(body))>"

Binding the HTTP method and request URI into the signed envelope prevents cross-endpoint replay: a signature captured on (e.g.) POST /provision can no longer be replayed against POST /deprovision, nor can a signature on GET /info/<id> be replayed against GET /logs/<id>. The body is included via its SHA-256 hash (bounded length, binary-safe) rather than as a literal string, so arbitrary bytes — including NUL, newlines, or non- UTF-8 — cannot influence the canonical string.

The timestamp is bound to the same envelope so an attacker cannot substitute a different timestamp on a captured request.

Replay protection

Verify takes a maxAge parameter — callers choose the freshness window for their use case (the callback path uses 5 minutes via DefaultCallbackMaxAge). The 1-minute clock-skew tolerance for future timestamps is hardcoded in Verify; use VerifyWithTime to override it.

The timestamp check bounds replay of the same signed request to maxAge; it does not provide one-time delivery because this protocol intentionally has no nonce cache. Binding method, URI, and body prevents a captured signature from being moved to a different endpoint, resource, method, or payload, while binding the timestamp prevents timestamp substitution. The canonical-string approach is conceptually similar to AWS SigV4's canonical request, simplified to the fields this codebase needs.

Constant-time comparison

Verification uses hmac.Equal (constant-time) to prevent timing attacks.

Key rotation

VerifyKeys holds the key a side signs with and, during a rotation, at most one verify-only key. It can verify but never sign. A signature is checked once for format and freshness, then against both keys (both MACs are always computed), and the result names the KeySlot that matched. Failures carry a closed FailureReason. Keys are compared by HMAC equivalence, since a key and its zero-padded copy authenticate the same messages.

Method and URI conventions

Method is case-sensitive (RFC 9110 §9.1) — callers pass it as-is, and no normalization is performed. Sender and verifier must agree on case; Go's net/http does NOT canonicalize method case (http.NewRequest("post", ...) preserves "post"; server parsing preserves whatever the client put on the wire). Agreement in this codebase comes from convention: every call site uses the http.MethodGet / http.MethodPost / ... named constants, which are uppercase. A caller that passes a literal "post" will produce a signature that does not verify against a verifier seeing "POST" — that is the intended behavior, since silent normalization would mask method-confusion bugs.

URI is the request-target: path plus "?<raw-query>" if a query string is present. On the verifier side, use r.URL.RequestURI(). On the sender side, use req.URL.RequestURI() after constructing the *http.Request — both sides then sign the bytes that go on the wire.

Used by

  • Backend → Fred callbacks (internal/api/callback_auth.go)
  • Fred → backend HTTP requests (internal/backend/client.go)
  • docker-backend self-verification middleware (cmd/docker-backend/main.go)
  • k3s-backend self-verification middleware (cmd/k3s-backend/server.go)
  • mock-backend self-verification middleware (cmd/mock-backend/main.go)

Index

Constants

View Source
const (
	// SignatureHeader is the HTTP header name for HMAC signatures.
	SignatureHeader = "X-Fred-Signature"

	// MinSecretLength is the minimum HMAC secret length accepted at production
	// authentication and signing boundaries. Thirty-two bytes retain the full
	// security strength of HMAC-SHA256.
	MinSecretLength = 32
)

Variables

This section is empty.

Functions

func ComputeMAC

func ComputeMAC(secret string, timestamp int64, method, uri string, body []byte) []byte

ComputeMAC computes the HMAC-SHA256 over the canonical string "<timestamp>\n<method>\n<uri>\n<hex(sha256(body))>".

body may be nil; sha256.Sum256(nil) is well-defined and produces the stable empty-body hash, so both sender and verifier always emit the hash field.

func Equivalent added in v0.14.0

func Equivalent(a, b string) bool

Equivalent reports whether a and b authenticate exactly the same messages.

func KeyID added in v0.14.0

func KeyID(key string) string

KeyID returns a short non-secret identifier for key, so operators can confirm two processes hold the same key without revealing it. Print it only on request: anyone holding an ID can test guesses of a weak key against it.

func MatchCallbackKeys added in v0.14.0

func MatchCallbackKeys(
	keys VerifyKeys,
	method, uri string,
	body []byte,
	signature, callbackPath string,
	maxAge, clockSkew time.Duration,
	now time.Time,
) error

MatchCallbackKeys checks a callback signature exactly as VerifyRoutedKeysWithTime does but issues no proof: nil means keys signed this callback, which grants nothing. It lets a verifier name the cause of a refusal it makes regardless, such as a callback from a fenced backend.

func NewCallbackProofBoundary added in v0.14.0

func NewCallbackProofBoundary() (CallbackProofVerifier, CallbackProofConsumer)

NewCallbackProofBoundary constructs matched verifier and consumer capabilities. Proof minted by any other boundary is rejected even when the request bytes and HMAC secret are identical.

func ParseSignature

func ParseSignature(signature string) (int64, string, bool)

ParseSignature parses a signature in the format "t=<timestamp>,sha256=<hex>". Returns the timestamp, hex-encoded signature, and whether parsing succeeded.

func Sign

func Sign(secret, method, uri string, body []byte) string

Sign computes an HMAC-SHA256 signature for the given request shape using the current time. Returns the signature in the format "t=<timestamp>,sha256=<hex>".

Callers building an outbound *http.Request should pass req.Method and req.URL.RequestURI() — both fields are populated by the stdlib request constructor and match what the verifier will see on the wire.

func SignRequest

func SignRequest(secret string, req *http.Request, body []byte) string

SignRequest signs the given request shape using the current time. Equivalent to Sign(secret, req.Method, req.URL.RequestURI(), body).

func SignWithTime

func SignWithTime(secret, method, uri string, body []byte, t time.Time) string

SignWithTime is Sign with an explicit timestamp (for tests and deterministic flows).

func Verify

func Verify(secret, method, uri string, body []byte, signature string, maxAge time.Duration) error

Verify verifies an HMAC-SHA256 signature against the given request shape. It checks signature format, timestamp freshness (within maxAge), and HMAC correctness over "<timestamp>\n<method>\n<uri>\n<hex(sha256(body))>". Uses hmac.Equal on raw bytes for constant-time comparison. Clock skew tolerance for future timestamps is 1 minute.

Verifiers should pass r.Method and r.URL.RequestURI() from the incoming *http.Request.

func VerifyRequest

func VerifyRequest(secret string, r *http.Request, body []byte, signature string, maxAge time.Duration) error

VerifyRequest verifies a signature against the given request shape. The caller must read r.Body first and pass it; this wrapper does not touch r.Body. Equivalent to Verify(secret, r.Method, r.URL.RequestURI(), body, signature, maxAge).

func VerifyRequestWithTime

func VerifyRequestWithTime(
	secret string,
	r *http.Request,
	body []byte,
	signature string,
	maxAge, clockSkew time.Duration,
	now time.Time,
) error

VerifyRequestWithTime is VerifyRequest with explicit reference time and clock-skew tolerance for future timestamps.

func VerifyWithTime

func VerifyWithTime(
	secret, method, uri string,
	body []byte,
	signature string,
	maxAge, clockSkew time.Duration,
	now time.Time,
) error

VerifyWithTime verifies an HMAC-SHA256 signature with an explicit reference time and configurable clock skew tolerance for future timestamps.

Types

type CallbackProofConsumer added in v0.14.0

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

CallbackProofConsumer accepts proof minted by exactly one paired verifier. It cannot verify a signature or mint proof itself, so giving it to the settlement coordinator does not also give that coordinator ingress authentication authority. Its zero value is invalid.

func (CallbackProofConsumer) Accepts added in v0.14.0

func (consumer CallbackProofConsumer) Accepts(request VerifiedRequest) bool

Accepts reports whether request was minted for this exact boundary.

func (CallbackProofConsumer) Valid added in v0.14.0

func (consumer CallbackProofConsumer) Valid() bool

Valid reports whether the consumer belongs to a constructed boundary.

type CallbackProofVerifier added in v0.14.0

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

CallbackProofVerifier is the sole capability that can mint callback proof for one process-local verification boundary. Its zero value is invalid. The HMAC key remains owned by the API keyring; this capability binds a successfully verified envelope to the exact settlement consumer assembled beside it by the composition root.

func (CallbackProofVerifier) Valid added in v0.14.0

func (verifier CallbackProofVerifier) Valid() bool

Valid reports whether the verifier belongs to a constructed boundary.

func (CallbackProofVerifier) VerifyRoutedKeysWithTime added in v0.14.0

func (verifier CallbackProofVerifier) VerifyRoutedKeysWithTime(
	keys VerifyKeys,
	method, uri string,
	body []byte,
	signature, route, callbackPath string,
	maxAge, clockSkew time.Duration,
	now time.Time,
) (VerifiedRequest, KeySlot, error)

VerifyRoutedKeysWithTime is VerifyRoutedWithTime for a side that may hold a rotation key: it accepts a signature made by either key in keys and reports which one matched.

func (CallbackProofVerifier) VerifyRoutedWithTime added in v0.14.0

func (verifier CallbackProofVerifier) VerifyRoutedWithTime(
	secret, method, uri string,
	body []byte,
	signature, route, callbackPath string,
	maxAge, clockSkew time.Duration,
	now time.Time,
) (VerifiedRequest, error)

VerifyRoutedWithTime verifies one callback request and, on success, mints immutable evidence containing the exact signed envelope and selected key route. callbackPath is the exact escaped path after applying any static reverse-proxy prefix; a signature for another endpoint cannot be promoted to callback authority.

type FailureReason added in v0.14.0

type FailureReason uint8

FailureReason is the closed cause of a failed verification. The zero value is invalid and never describes a failure.

const (

	// FailureFormat is a malformed signature header or a request envelope that
	// does not match the endpoint.
	FailureFormat FailureReason
	// FailureExpired is a timestamp older than the freshness window.
	FailureExpired
	// FailureFuture is a timestamp beyond the allowed clock skew.
	FailureFuture
	// FailureMismatch is a well-formed, fresh signature no configured key made.
	FailureMismatch
)

func FailureReasonOf added in v0.14.0

func FailureReasonOf(err error) FailureReason

FailureReasonOf returns the closed cause of a verification failure, or the invalid zero value for any other error.

func (FailureReason) String added in v0.14.0

func (reason FailureReason) String() string

String returns the reason's metric label; the invalid zero value has none.

type KeySlot added in v0.14.0

type KeySlot uint8

KeySlot names which configured key verified a request. The zero value is invalid and never describes a successful verification.

const (

	// KeySlotCurrent is the key this side also signs with.
	KeySlotCurrent KeySlot
	// KeySlotRotation is the one verify-only key a side may hold while its
	// shared key rotates.
	KeySlotRotation
)

func VerifyRequestKeys added in v0.14.0

func VerifyRequestKeys(
	keys VerifyKeys,
	r *http.Request,
	body []byte,
	signature string,
	maxAge time.Duration,
) (KeySlot, error)

VerifyRequestKeys verifies r against keys with a one-minute future clock skew and reports which key matched. The caller must read r.Body first and pass it.

func (KeySlot) Valid added in v0.14.0

func (slot KeySlot) Valid() bool

Valid reports whether slot names a configured key.

type VerificationError added in v0.14.0

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

VerificationError is a failed verification and its closed cause. Its message is the text the untyped verification errors carried, so callers and logs see the same diagnostics.

func (*VerificationError) Error added in v0.14.0

func (err *VerificationError) Error() string

type VerifiedRequest added in v0.14.0

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

VerifiedRequest is immutable evidence that one exact HTTP request envelope passed HMAC verification. Its zero value is invalid and its fields are deliberately private: application code can inspect a verified request but cannot manufacture one from an already-decoded DTO.

route identifies the key selected before verification (for example a backend storage lineage). Binding it into the proof prevents a caller from authenticating with one key and later attributing the same bytes to another key route.

func (VerifiedRequest) Body added in v0.14.0

func (request VerifiedRequest) Body() []byte

Body returns a detached copy of the exact HMAC-covered body bytes.

func (VerifiedRequest) Format added in v0.14.0

func (request VerifiedRequest) Format(state fmt.State, _ rune)

Format prevents generic formatting, including reflective %+v and %#v formatting, from exposing the authenticated URI, body, or selected key route. Explicit accessors remain available to the narrowly scoped consumer.

func (VerifiedRequest) KeyRoute added in v0.14.0

func (request VerifiedRequest) KeyRoute() string

KeyRoute returns the immutable route whose key authenticated this request.

func (VerifiedRequest) LogValue added in v0.14.0

func (request VerifiedRequest) LogValue() slog.Value

LogValue keeps structured logging on the same redacted representation as generic formatting. In particular, slog must not reflect over private proof fields when a proof is passed through slog.Any.

func (VerifiedRequest) Method added in v0.14.0

func (request VerifiedRequest) Method() string

Method returns the exact HMAC-covered HTTP method.

func (VerifiedRequest) URI added in v0.14.0

func (request VerifiedRequest) URI() string

URI returns the exact HMAC-covered canonical request URI.

func (VerifiedRequest) Valid added in v0.14.0

func (request VerifiedRequest) Valid() bool

Valid reports whether the request was minted by this package's verifier.

func (VerifiedRequest) ValidCallback added in v0.14.0

func (request VerifiedRequest) ValidCallback() bool

ValidCallback reports whether the proof was minted by the callback-specific verifier after checking the exact configured callback path.

type VerifyKeys added in v0.14.0

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

VerifyKeys is the set of keys one side accepts: the key it signs with and, during a rotation, at most one verify-only key. It can verify but never sign, and it cannot hold a third key. The zero value is invalid.

func NewVerifyKeys added in v0.14.0

func NewVerifyKeys(current, rotation string) (VerifyKeys, error)

NewVerifyKeys builds the keys one side accepts. rotation is optional; when set it must be as long as a signing key and must not be equivalent to current, since an equivalent key would add no new authority and hide a configuration mistake.

func (VerifyKeys) CurrentKeyID added in v0.14.0

func (keys VerifyKeys) CurrentKeyID() string

CurrentKeyID returns the KeyID of the signing key.

func (VerifyKeys) HasRotation added in v0.14.0

func (keys VerifyKeys) HasRotation() bool

HasRotation reports whether a verify-only rotation key is configured.

func (VerifyKeys) RotationKeyID added in v0.14.0

func (keys VerifyKeys) RotationKeyID() (string, bool)

RotationKeyID returns the KeyID of the verify-only key, if configured.

func (VerifyKeys) SharesKeyWith added in v0.14.0

func (keys VerifyKeys) SharesKeyWith(other VerifyKeys) bool

SharesKeyWith reports whether any key of keys is equivalent to any key of other. Two backends must never accept a common key.

func (VerifyKeys) Valid added in v0.14.0

func (keys VerifyKeys) Valid() bool

Valid reports whether keys was built by NewVerifyKeys.

Jump to

Keyboard shortcuts

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