Authentication host v1

September 18, 2026 · View on GitHub

Status: private Rust embedding profile for password hashing and session service; local evidence, no network or production deployment claim. Audience: host integrators and compiler contributors implementing authenticated embedding.

This Rust embedding profile adds actual password hashing and authenticated sessions alongside the pure std.auth predicates specified in Authentication and Sessions v1. It adds no source-language operation or ambient compiler authority.

The host explicitly supplies entropy, signing key, clock ticks and storage. SecretBytes has redacted debug output, no serialization or clone traits, and zeroizes owned storage on drop. This does not erase caller-owned copies. Errors are closed tags and carry no credential or bearer-token contents. authentication::tests::secret_bytes_debug_never_prints_its_content pairs a non-vacuity control (the marker bytes are really present in the owned storage) with the redaction assertion (the marker is absent from every byte window of the rendered Debug output), rather than checking only a fixed positive string.

Reference in-memory stores provide bounded process-local storage; they do not claim database durability, distributed transactions, HTTP middleware, rate limiting, or production deployment support.

Signed session service

authentication::session provides a host-owned session flow with explicit entropy, signing key, clock tick, and SessionStore. SessionService fixes HS256 at construction. No bearer token selects an algorithm, key, issuer, audience, or policy identity. SessionPolicy binds stable exact policy, issuer, and audience strings and positive bounded TTLs.

The bearer value is a semaprax-session-token.v1, not a JWT: canonical fixed binary claims and a fixed HMAC-SHA-256 tag are independently base64url-no-pad encoded and separated by one dot. The authenticated claims carry the schema magic/version, 32-byte session id, generation, issue and expiry ticks, and the exact policy, issuer, audience, and subject strings. Parsing is bounded and canonical; MAC verification uses hmac's verify_slice before claims are accepted. The tag includes a fixed domain separator. Verification rejects a clock tick before issue time and rechecks the claimed lifetime against the current server policy.

SessionToken owns redacted, zeroized bytes and exposes its value only through bearer() for an explicitly selected transport boundary. It has no Clone, Debug, or serialization implementation. SecretBytes likewise never exposes the server signing key outside the authentication module.

SessionStore requires atomic issue_if_absent, compare_and_rotate, and compare_and_revoke operations. Rotation retires the presented record and creates a fresh random 32-byte id with an incremented generation in one store operation; revocation and rotation refuse stale, revoked, and generation-mismatched records. InMemorySessionStore is a capped process-local reference implementation. It is neither a durable database nor a distributed transaction implementation.

The service has no source-language operation, ambient clock, entropy, key, filesystem, network, HTTP middleware, authorization policy, rate limit, or production external-store implementation. A successful authentication result returns only the subject and public issue/expiry/generation claims; it never returns a bearer token or signing key.

Signing key rotation

SessionService::with_key_rotation accepts one optional retired key wrapped in SigningKeyRotation::new(key, grace_until). New tokens are always signed with the current key; a token signed with the retired key still verifies while the caller's now has not yet passed grace_until, after which it is refused identically to an unknown key. This mirrors std.auth's pure token_key_is_current_or_in_grace policy at the host layer: rotating the server signing key does not need to invalidate every live session immediately, and the bound is explicit and caller-supplied, never open-ended. A rotation cannot name the same key as both current and previous. authentication::session::tests:: signing_key_rotation_accepts_the_old_key_only_within_its_grace_window proves a pre-rotation token verifies within the grace window and is refused past it, that a party holding only the new key cannot verify a pre-rotation token, and that a party holding only the retired key (with no configured grace) cannot verify a post-rotation token.

Hostile input and concurrent sessions

authentication::session::tests:: oversized_truncated_and_control_byte_bearer_tokens_are_refused and authentication::session::tests:: control_bytes_and_unicode_confusable_subjects_are_refused_at_issue cover oversized bearer strings, truncated tokens missing their tag, an empty bearer, embedded NUL and control bytes, and a Unicode-confusable subject (Cyrillic "а" for Latin "a") that is refused when malformed and kept byte-distinct rather than normalized when well-formed. authentication::session::tests:: concurrent_sessions_for_the_same_subject_are_independent proves the store admits more than one simultaneously active session per subject and that revocation is per-session, not per-subject.

Password records

authentication::password is a native host service. It admits only RustCrypto Argon2id version 19 records, gives the KDF a 16-byte host-supplied salt, and stores a 32-byte output. A password input is at most 1,024 bytes and a PHC record is at most 512 bytes.

The default policy is 19,456 KiB, two iterations, and parallelism one. The only accepted resource envelope is 19,456--65,536 KiB, two--four iterations, and parallelism one. A PasswordHasherHost creates hashes with its current policy and verifies only that policy plus at most four explicit, bounded migration policies.

StoredPasswordHash::parse_for_storage parses the entire PHC envelope before any KDF work: its algorithm and version are fixed, parameter keys are closed, salt and digest encodings are canonical and fixed-size, and all resource bounds are checked. Its debug representation is redacted; storage adapters receive a record only through expose_for_storage. Incorrect passwords and malformed stored credentials are returned as closed AuthError tags. This host API does not add a source-language secret type, password operation, or authority.

PasswordHasherHost::with_approved_migrations lets a deployment verify a record hashed under a previously current policy while every new hash uses the active policy: authentication::password::tests:: approved_migration_verifies_old_hash_but_new_hashes_always_use_current_policy hashes under an old policy, shows an unapproved-migration hasher refuses it, then shows an approving hasher verifies the legacy record, still rejects a wrong password against it, and rehashes under the current policy rather than the migration policy. The migration set itself is bounded (InvalidPolicy at five entries or when it names the active policy). authentication::password::tests:: policy_bounds_reject_out_of_range_iterations_and_parallelism asserts the floor and ceiling on iterations and refuses any parallelism other than one, each individually, as a closed-form resource-exhaustion bound rather than a silent clamp.

Account and session composition

service::AuthService performs signup by hashing a supplied secret before an atomic AccountStore::insert_if_absent; storage failure never issues a token. Login verifies the actual stored hash before asking SessionService to issue a token. protected, rotate and logout delegate authenticated session validation and atomic session transitions. Account identifiers are bounded ASCII identifiers (letters, digits, ., _, -, @), at most 128 bytes. The reference account store holds at most 4096 entries under a lower explicit host capacity. The host supplies transaction semantics for its own store.

OsAuthEntropy is an explicitly instantiated native capability. Tests inject deterministic entropy; that fixture is not a deployment random generator. Clock values are explicit host ticks and must use one consistent trusted clock. Hosts must bound simultaneous KDF calls and apply login/signup rate limits. Unknown accounts and wrong passwords share an error tag, but this profile does not promise indistinguishable response timing or conceal signup conflicts.

The executable composition gate is authentication::service::tests::actual_signup_login_rotation_protected_logout. It performs real hashing, wrong-password rejection, login, protected access, rotation, old-token refusal and logout. It is a Rust embedding test, not an HTTP deployment or a source-language/backend conformance claim.

Audit events

authentication::audit::AuthAuditEvent is a closed, redaction-safe record: a closed AuthAuditKind (Signup, Login, SessionVerify, SessionRotate, Logout), a closed AuthAuditOutcome (Allowed or Denied(AuthError)), a bounded pseudonymous subject identifier re-validated at construction, and a host-supplied timestamp tick. AuthAuditOutcome::from_result derives the outcome directly from an existing AuthService call's Result without a host needing to re-derive the allow/deny decision. No constructor on this type accepts a SecretBytes, SessionToken, or StoredPasswordHash; its Display and derived Debug need no redaction because every field was already established as non-secret when the event was built, not because a value was hidden after being stored. authentication::audit::tests::audit_event_is_complete_and_never_carries_secret_material pairs a non-vacuity control (a real Argon2id hash and a real wrong-password verification failure both actually happen, using a bytes marker) with a negative assertion that neither the marker, the stored PHC record, nor the rejected password appears in the event's rendered Display or Debug output. This is a host-layer building block a caller composes after calling AuthService; it does not itself write to any sink, matching std.log's existing, unmodified role for that.