Expand description
Credential hashing: the selector and stored-hash formats of the credential schema contract (DW-018/DW-019, peppered per #124).
The lookup SELECTOR (hex(sha256(value))) and the stored-hash formats
are shared by every credential holder — config-seeded consumers hashed
at startup, the state store’s credential records, and the
authenticator’s presentation path — so the formats are defined here
with the schema instead of in any one consumer. A selector is never
plaintext: an indexed store of selectors leaks nothing about key
material.
§Stored-hash formats (#124)
hmac-sha256:<hex(HMAC-SHA256(pepper, secret)))>— the PEPPERED format every NEW write uses when a per-deployment pepper is configured (resolved through theSecretSourceextension seam and threaded down as raw bytes; the security domain never touches the extension itself). A state-DB leak alone cannot verify guesses: the search also needs the pepper. Verification is constant-time over the hex encodings, exactly like the sha256 path.sha256:<hex(sha256(secret))>— the LEGACY unpeppered format. Existing entries keep verifying during the transition; on successful legacy verification with a pepper configured, the store path re-hashes the row to the peppered format in place (the state domain’sStateStore::rehash_credential; config cannot link the state path — dependency direction is downward only), so the transition completes lazily without credential re-issue.- PHC argon2id strings (
$argon2id$...) — the memory-hard store-managed Basic path, unchanged by the pepper.
The SELECTOR stays hex(sha256(value)) in both modes deliberately:
it must remain computable from the presented material alone (no
secret input) or indexed lookups could not find the row, and a
sha256 selector leaks nothing about the secret. The pepper is SECRET
material: never logged, never in Debug, never in error text.
§Secret references (DW-045)
A secret-bearing config field (today: consumers[].credentials[]
api_key.key) may carry either the value INLINE or a ${...}
REFERENCE resolved through the file/env vocabulary this module
defines:
${ENV_NAME}— an environment variable ([A-Za-z_][A-Za-z0-9_]*; thefile:andredactedprefixes below are reserved and matched first, so they cannot collide with a variable name). Unset or empty fails closed.${file:/path/to/secret}— a file read at resolution time (the Docker/Kubernetes mounted-secret and systemdLoadCredentialshape). ONE trailing newline is trimmed (\nor\r\n) — the convention of mounted secret files — and the remainder must be non-empty. The read is BOUNDED atMAX_SECRET_FILE_BYTES(1 MiB): a larger file fails closed naming the path and the limit.${redacted:sha256:<8hex>}— the REDACTION PLACEHOLDER this module’sredact_inline_secretemits wherever the gateway echoes configuration (adminGET /config, DW-045). It is not resolvable BY DESIGN: a GET-then-PATCH round trip that carries a placeholder back is rejected by validation with a precise issue instead of silently installing placeholder bytes as a live key. A short sha256 prefix lets operators compare WHICH key is deployed without seeing it.
Posture (pinned by tests): INLINE VALUES REMAIN ACCEPTED for
backward compatibility — existing configs keep working — but they
are redacted in every config echo; references are the recommended
shape for new configs because the config file then never holds the
secret bytes at all. A value that merely CONTAINS ${...} mid-string
is NOT a reference (no shell-style partial expansion; it stays a
literal), while a value that STARTS with ${ but is not a
well-formed reference — including one that is never closed, like
${unclosed — is a validation error, never a silently installed
literal key.
Read-time model: references resolve at CONFIG-COMPILE time — cold
start and every hot reload/re-publish re-read the env/file (the same
generation-follows-config contract as TLS cert material; a rotation
needs a reload to apply). Validation resolves every reference and
fails closed with a ValidationIssue naming the field; the
credential consumers (authn registry build, store seeding) resolve
again immediately after publish, so a secret that breaks between
validate and build is skipped loudly (the microsecond-race
fail-closed backstop, the same pattern as trusted_ca_file). The
request path NEVER touches a secret source: resolved values are
hashed into selectors/stored-hashes at build time and the plaintext
is dropped. The SecretSource extension impls (EnvSecretSource,
FileSecretSource) share this module’s reading rules so the seam
and the config grammar cannot drift apart.
Enums§
- Secret
Ref - A parsed
${...}secret reference (DW-045). See the module docs for the grammar, the read-time model, and the redaction-placeholder round-trip contract.
Constants§
- MAX_
SECRET_ FILE_ BYTES - Upper bound on one secret file’s size (DW-045, #46): secret files
hold short key material, and the read must never be a
memory-exhaustion vector —
${file:/dev/zero}reads NUL bytes forever (NULs are valid UTF-8), so an unbounded read runs until the allocator gives up and takes the process with it, on every reload. Bounded reads have precedent: the SNI ClientHello reassembler caps at 64 KiB (MAX_HELLO_BYTESin the security/TLS domain). At or under the cap the file is read whole; anything larger fails closed naming the path and the limit. The bound is enforced BOTH on the file’s metadata up front and on the bytes actually read (Read::take), so a file that grows between the two checks — or a non-regular file whose metadata size is 0, like/dev/zero— is still capped.
Functions§
- credential_
selector - Lookup selector for API-key and Basic credentials:
hex(sha256(value)). The selector is therefore a HASH, never plaintext — an indexed store of selectors leaks nothing about key material (this closes the DW-018 finding that config-seeded selectors were the raw config values). - hmac_
stored_ hash - Peppered stored-hash format (#124):
hmac-sha256:<hex(HMAC-SHA256(pepper, secret))>. Used for every NEW credential write (config seeding, store re-hash on legacy verify) when a pepper is configured; without one the legacysha256_stored_hashformat remains in force. An HMAC key shorter than the block size weakens the construction, so deployments should use at least 32 bytes of entropy (not enforced: a short pepper still strictly dominates no pepper). - parse_
secret_ reference - Classify a configured secret string (DW-045):
- read_
secret_ file - Read one secret file (DW-045): the whole file as UTF-8 with a SINGLE
trailing newline trimmed (
\nor\r\n— the mounted-secret-file convention; interior newlines are preserved), bounded atMAX_SECRET_FILE_BYTES. Shared by the config grammar and theFileSecretSourceextension impl so the two cannot drift. The error names the path, never any content. - redact_
inline_ secret - The redaction placeholder for an INLINE secret value (DW-045):
${redacted:sha256:<first 8 hex of sha256>}. The short prefix lets an operator compare which key a generation carries without ever seeing the key; the placeholder itself is unresolvable by design, so echoing it back through a config-publishing surface is rejected fail-closed instead of installing placeholder bytes as a live key. Reference-shaped values are returned UNCHANGED (an env-var name or file path is not secret bytes — the config file already carries it). - resolve_
configured_ secret - Resolve a configured secret string (DW-045): literals pass through untouched; well-formed references resolve at CALL time; malformed reference-shaped values fail closed. Every consumer of secret-bearing config fields (snapshot validation, the authn registry build, store seeding) goes through this one entry point so no path can bypass resolution.
- sha256_
hex - sha256 digest, hex-encoded (the API-key/Basic hashing path).
- sha256_
stored_ hash - Stored-hash format for the fast path:
sha256:<hex(sha256(secret))>.