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
- func ComputeMAC(secret string, timestamp int64, method, uri string, body []byte) []byte
- func Equivalent(a, b string) bool
- func KeyID(key string) string
- func MatchCallbackKeys(keys VerifyKeys, method, uri string, body []byte, ...) error
- func NewCallbackProofBoundary() (CallbackProofVerifier, CallbackProofConsumer)
- func ParseSignature(signature string) (int64, string, bool)
- func Sign(secret, method, uri string, body []byte) string
- func SignRequest(secret string, req *http.Request, body []byte) string
- func SignWithTime(secret, method, uri string, body []byte, t time.Time) string
- func Verify(secret, method, uri string, body []byte, signature string, ...) error
- func VerifyRequest(secret string, r *http.Request, body []byte, signature string, ...) error
- func VerifyRequestWithTime(secret string, r *http.Request, body []byte, signature string, ...) error
- func VerifyWithTime(secret, method, uri string, body []byte, signature string, ...) error
- type CallbackProofConsumer
- type CallbackProofVerifier
- func (verifier CallbackProofVerifier) Valid() bool
- func (verifier CallbackProofVerifier) VerifyRoutedKeysWithTime(keys VerifyKeys, method, uri string, body []byte, ...) (VerifiedRequest, KeySlot, error)
- func (verifier CallbackProofVerifier) VerifyRoutedWithTime(secret, method, uri string, body []byte, signature, route, callbackPath string, ...) (VerifiedRequest, error)
- type FailureReason
- type KeySlot
- type VerificationError
- type VerifiedRequest
- func (request VerifiedRequest) Body() []byte
- func (request VerifiedRequest) Format(state fmt.State, _ rune)
- func (request VerifiedRequest) KeyRoute() string
- func (request VerifiedRequest) LogValue() slog.Value
- func (request VerifiedRequest) Method() string
- func (request VerifiedRequest) URI() string
- func (request VerifiedRequest) Valid() bool
- func (request VerifiedRequest) ValidCallback() bool
- type VerifyKeys
Constants ¶
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 ¶
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
Equivalent reports whether a and b authenticate exactly the same messages.
func KeyID ¶ added in v0.14.0
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 ¶
ParseSignature parses a signature in the format "t=<timestamp>,sha256=<hex>". Returns the timestamp, hex-encoded signature, and whether parsing succeeded.
func Sign ¶
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 ¶
SignRequest signs the given request shape using the current time. Equivalent to Sign(secret, req.Method, req.URL.RequestURI(), body).
func SignWithTime ¶
SignWithTime is Sign with an explicit timestamp (for tests and deterministic flows).
func Verify ¶
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).
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.
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.