entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
# entropy-auth

Authentication and authorization primitives for Entropy Softworks Rust projects. Crypto, encoding, and protocol logic is implemented from specifications, leaning only on well-audited, idiomatic crates.io dependencies (such as `argon2`, `aes-gcm`, `webauthn-rs`, the RustCrypto curve crates, `serde`/`serde_json`, `rand_core`, and `tracing` — see the dependency table in the API docs for the full set) — no internal-only dependencies. Logging is emitted through the [`tracing`](https://crates.io/crates/tracing) facade, so consumers wire up whatever subscriber they like. Thread-safe (`Send + Sync`).

## Installation

Releases use CalVer (`YYYY.M.D`), stamped at publish from the build date, so
pin to the year-and-month series you want:

```toml
[dependencies]
entropy-auth = "2026.6"

# Optional protocol modules
# entropy-auth = { version = "2026.6", features = ["oidc", "saml"] }
```

## Quick Start

### Password Hashing

```rust,no_run
use entropy_auth::{PasswordHash, PasswordConfig};

let config = PasswordConfig::default();
let hash = PasswordHash::generate(b"my-secret-password", &config).unwrap();
assert!(hash.verify(b"my-secret-password"));
assert!(!hash.verify(b"wrong-password"));

// Serialize for storage
let stored = hash.to_phc_string();
let loaded = PasswordHash::parse(stored).unwrap();
assert!(loaded.verify(b"my-secret-password"));
```

### API Key Authentication

```rust,no_run
use entropy_auth::{ApiKey, ApiKeyHash};

let (key, hash) = ApiKey::generate().unwrap();
println!("Give to client: {}", key.as_str());
println!("Store in DB: prefix={}", hash.prefix());

// Later, verify:
assert!(key.verify(&hash));
```

### OAuth 2.0 Flow

```rust,no_run
use entropy_auth::oauth::{OAuthConfig, AuthorizationRequest};

let config = OAuthConfig::builder("client-id", "client-secret")
    .authorization_endpoint("https://auth.example.com/authorize")
    .token_endpoint("https://auth.example.com/token")
    .redirect_uri("https://myapp.com/callback")
    .scope("openid")
    .scope("profile")
    .scope("email")
    .build()
    .unwrap();

let auth_req = AuthorizationRequest::build(&config).unwrap();
println!("Redirect user to: {}", auth_req.url());
// After callback, exchange code for tokens...
```

## Feature Flags

Security-related capabilities are **always on** per the project's "no feature flags for core security" policy. The remaining flags gate optional protocol or use-case modules:

| Feature | Default | Description |
|---------|---------|-------------|
| `saml` | no | SAML 2.0 support (includes XML parser) |
| `oidc` | no | OpenID Connect support (enables `asym-jwt`) |
| `asym-jwt` | no | Asymmetric JWT: EdDSA/ES256 signing + verification, RS256/RS512 **verification** (RFC 7518 §3.3), and JWKS (RFC 7515/7517/7518/7638/8037); pulls in the RustCrypto curve crates plus `rsa` + `sha2` |
| `ssh-keys` | no | SSH public key parsing and fingerprinting |

Always-on capabilities (no flag required): local auth, OAuth 2.0, API auth, sessions, JWT (HMAC HS256/HS512), **TOTP / MFA**, **HIBP password breach checking**, **AES-GCM secret-box**, **WebAuthn / passkey ceremonies**. Build dependencies include `argon2`, `tracing`, `aes-gcm`, `rand_core`, `webauthn-rs` (transitively `openssl-sys` — consumers need `openssl-devel` or equivalent at build time), `serde`, `serde_json`. Enabling `asym-jwt` (or `oidc`) additionally pulls in the optional `ed25519-dalek` and `p256` crates for EdDSA/ES256 (the `ecdsa` and `signature` traits arrive transitively via `p256`), plus `rsa` + `sha2` for verifying RSA-signed (RS256/RS512) ID tokens from external OIDC providers (Google, Microsoft, Okta). RSA is **verify-only** — the crate validates RSA-signed tokens but never mints them — so the `rsa` private-key path (and RUSTSEC-2023-0071's Marvin timing concern, which is specific to private-key operations) is never exercised.

## Passkeys / WebAuthn

The crate wraps [`webauthn-rs`](https://docs.rs/webauthn-rs) in the same trait-and-builder shape used by the rest of the crate. Always available — no feature flag required:

```toml
[dependencies]
entropy-auth = "2026.6"
```

This pulls in `webauthn-rs` (and its transitive dependencies, including OpenSSL). All consumers of `entropy-auth` get passkey support out of the box.

### Public surface

* `RelyingParty` / `RelyingPartyBuilder` — RP-ID, RP-name, and origin allowlist; builder validation rejects empty RP-IDs, missing origins, and unparseable origin URLs at build time.
* `PasskeyCredential` — stored credential record (credential ID, embedded `Passkey`, sign-count, transports, AAGUID, label, PRF flag and salt, timestamps).
* `PasskeyCredentialStore` — trait that mirrors `CredentialStore`: one associated `type Error: std::error::Error`, pluggable persistence methods (`lookup_by_user`, `lookup_by_credential_id`, `store`, `update_sign_count`, `update_last_used`, `remove`).
* `InMemoryPasskeyStore` — `Vec`-backed implementation for tests and prototypes (uses `constant_time_eq` for all credential-ID / user-handle comparisons).
* `RegistrationCeremony::begin` / `RegistrationCeremony::finish` — two-step registration; returns a `RegistrationOutcome` carrying `prf_supported: bool`.
* `AuthenticationCeremony::begin` / `AuthenticationCeremony::finish` — two-step authentication; returns an `AuthenticationOutcome` with the new sign-count and PRF flag.
* `WebAuthnError` — private-enum + public-struct error wrapper with `is_*` query methods (`is_origin_mismatch`, `is_rp_id_mismatch`, `is_sign_count_rollback`, `is_credential_not_found`, …).
* `prf` module — `PrfSalt`, `PrfRegistrationRequest`, `PrfAuthenticationRequest`, `PrfClientResult`, `inspect_registration_response`, `inspect_authentication_response`.

### PRF (prefer-not-require)

The W3C WebAuthn Level 3 `prf` extension is requested at registration but never required. The authenticator may decline; that is **not** an error. The flag is surfaced on `RegistrationOutcome::prf_supported`, persisted on `PasskeyCredential::prf_supported`, and re-checked on every authentication.

* PRF-capable credentials carry a 32-byte salt; the same salt is replayed on every authentication, the authenticator derives the same 32-byte PRF output, and the client uses it as a key-encryption key for the symmetric vault key. The server never sees the PRF output or the unwrapped key — the zero-knowledge property is preserved.
* Non-PRF credentials are still valid auth factors; the client falls back to a master-password unlock after the session is established.

Because `webauthn-rs` 0.5.x does not expose the W3C `prf` extension directly (it surfaces only the lower-level CTAP2 `hmac-secret`), this module layers PRF on top of the upstream challenge JSON. The documented happy-path API is `RegistrationChallenge::merged_challenge_json()` / `AuthenticationChallenge::merged_challenge_json()` — both return the `webauthn-rs` JSON with the PRF extension already spliced under `publicKey.extensions.prf`. The raw accessors `challenge_json()` and `prf_extension_json()` remain available for callers that need to perform the splice manually. On the response side, the consumer extracts the `clientExtensionResults.prf` block and passes it back via `client_extension_results: Option<&Value>`. See the module docs for the wire-format contract.

### Security notes

* `deny(unsafe_code)` crate-wide; the `webauthn` module adds no new `unsafe` blocks.
* `PrfSalt` is wrapped in `Zeroizing<Vec<u8>>` and redacted in `Debug` output.
* Sign-count rollback is rejected with `WebAuthnError::is_sign_count_rollback`; the in-crate unit tests pin the variant.
* Origin and RP-ID mismatches surface as distinct `WebAuthnError` variants so the consumer's HTTP error mapper can produce specific error codes for operator telemetry. Callers MUST collapse all WebAuthn failures into a single opaque "authentication failed" response over the wire to prevent credential enumeration.
* Missing PRF support is a flag, never an error. There is no `is_prf_not_supported` predicate by design — see the rollout plan §2.2 for the prefer-not-require rationale.

## Security

All cryptographic implementations are tested against published test vectors (NIST CAVP, RFC 4231, RFC 6238). Security-relevant design decisions:

- Secret material is stored in `Zeroizing<T>` wrappers that clear memory on drop.
- All secret comparisons use `constant_time_eq` to prevent timing side-channel attacks.
- `deny(unsafe_code)` crate-wide. The only `unsafe` is `core::ptr::write_volatile` for secret-scrubbing in `crypto/zeroize.rs`, `crypto/sha1.rs`, and `crypto/sha2.rs` (scoped `#[allow(unsafe_code)]`); the CSPRNG goes through `rand_core::OsRng`, which contains no `unsafe`.
- `AuthError::InvalidCredentials` is a single opaque variant for all authentication failures, preventing user enumeration.
- PKCE uses S256 only (plain is rejected).
- Minimum Argon2id parameters (memory, iterations, parallelism) are enforced by `PasswordConfig`.
- No secrets appear in `Debug` or `Display` output.

See [`SECURITY.md`](SECURITY.md) for the full security policy.

## Public API

Commonly-used exports, grouped by area. This is a curated selection, not the
complete surface — run `cargo doc --all-features --open` for everything
(including the `oauth::server` IdP types and the `asym-jwt` JWKS surface).
Items marked `core` are always available; others require the listed feature.

| Export | Kind | Feature | Description |
|--------|------|---------|-------------|
| `Sha256` | struct | core | SHA-256 digest. |
| `Sha512` | struct | core | SHA-512 digest. |
| `Sha1` | struct | core | SHA-1 digest (legacy interop; used by HIBP). |
| `HmacSha256` | struct | core | HMAC-SHA256. |
| `HmacSha512` | struct | core | HMAC-SHA512. |
| `HmacSha1` | struct | core | HMAC-SHA1. |
| `SecretBox` | struct | core | AES-256-GCM authenticated secret-box for data at rest. |
| `SecretBoxError` | struct | core | Secret-box encrypt/decrypt error. |
| `fill_random` | fn | core | Fill a buffer from the platform CSPRNG. |
| `random_token_hex` | fn | core | Generate a hex-encoded random token. |
| `random_token_base64url` | fn | core | Generate a base64url-encoded random token. |
| `RandomError` | struct | core | CSPRNG failure error. |
| `base64_encode` | fn | core | Standard base64 encoding. |
| `base64_decode` | fn | core | Standard base64 decoding. |
| `base64url_encode` | fn | core | URL-safe base64 encoding. |
| `base64url_decode` | fn | core | URL-safe base64 decoding. |
| `base32_encode` | fn | core | RFC 4648 base32 encoding (TOTP secrets). |
| `base32_decode` | fn | core | RFC 4648 base32 decoding. |
| `Base32DecodeError` | struct | core | Base32 decode error. |
| `hex_encode` | fn | core | Hex encoding. |
| `hex_encode_upper` | fn | core | Uppercase hex encoding. |
| `hex_decode` | fn | core | Hex decoding. |
| `url_encode` | fn | core | URL percent-encoding. |
| `url_decode` | fn | core | URL percent-decoding. |
| `url_encode_component` | fn | core | URL component encoding. |
| `Base64DecodeError` | struct | core | Base64 decode error. |
| `HexDecodeError` | struct | core | Hex decode error. |
| `UrlDecodeError` | struct | core | URL decode error. |
| `xml_escape` | fn | `saml` | Escape special characters for XML embedding. |
| `constant_time_eq` | fn | core | Timing-safe byte comparison. |
| `Zeroizing` | struct | core | Wrapper that clears memory on drop. |
| `ZeroizeOnDrop` | trait | core | Trait for types that zeroize on drop. |
| `Timestamp` | struct | core | UTC timestamp via Hinnant civil-date algorithm. |
| `is_valid_username` | fn | core | Username validation. |
| `is_valid_name` | fn | core | Name validation. |
| `is_valid_client_id` | fn | core | OAuth client ID validation. |
| `is_valid_redirect_uri` | fn | core | Redirect URI validation. |
| `is_valid_scope` | fn | core | OAuth scope validation. |
| `PasswordHash` | struct | core | Argon2id memory-hard password hashing. |
| `PasswordConfig` | struct | core | Password hashing configuration. |
| `PasswordError` | struct | core | Password hashing error. |
| `Session` | struct | core | Session token with expiry and idle timeout. |
| `SessionConfig` | struct | core | Session configuration. |
| `SessionError` | struct | core | Session error. |
| `SessionValidation` | enum | core | Session validation result. |
| `generate_token` | fn | core | Generate a random opaque token. |
| `generate_token_with_hash` | fn | core | Generate a token and its SHA-256 hash. |
| `hash_token_for_lookup` | fn | core | Re-derive a minted token's SHA-256 storage hash for server-side lookup. |
| `Username` | struct | core | Validated username wrapper. |
| `Credentials` | struct | core | Username + password pair. |
| `StoredCredential` | struct | core | Stored credential (hash + metadata). |
| `CredentialStore` | trait | core | Trait for credential storage backends. |
| `InMemoryCredentialStore` | struct | core | In-memory credential store. |
| `InMemoryStoreError` | struct | core | Error type for `InMemoryCredentialStore`. |
| `ApiKey` | struct | core | API key (cleartext, for client). |
| `ApiKeyHash` | struct | core | API key hash (for server-side storage). |
| `ApiKeyError` | struct | core | API key error. |
| `HmacRequestSigner` | struct | core | HMAC request signing. |
| `HmacRequestVerifier` | struct | core | HMAC request verification. |
| `HmacAuthError` | struct | core | HMAC auth error. |
| `BearerError` | struct | core | Bearer token extraction error. |
| `extract_bearer_token` | fn | core | Extract bearer token from `Authorization` header. |
| `JsonValue` | enum | core | Minimal JSON value (RFC 8259). |
| `JsonParseError` | struct | core | JSON parse error. |
| `JwtAlgorithm` | enum | core | JWT algorithm (`HS256`, `HS512`). |
| `JwtHeader` | struct | core | JWT header. |
| `JwtHeaderError` | struct | core | JWT header parse error. |
| `JwtClaims` | struct | core | JWT claims. |
| `JwtClaimsError` | struct | core | JWT claims parse error. |
| `JwtSignatureError` | struct | core | JWT signature error. |
| `JwtEncoder` | struct | core | Mint (sign) a JWT. |
| `JwtSigningAlgorithm` | enum | core | Algorithm for minting a JWT (`HS256`/`HS512`). |
| `IntoCustomClaim` | trait | core | Convert a value into a custom JWT claim. |
| `verify_jwt` | fn | core | Verify a JWT string. |
| `OAuthConfig` | struct | core | OAuth 2.0 client configuration. |
| `OAuthConfigBuilder` | struct | core | Builder for `OAuthConfig`. |
| `OAuthConfigError` | struct | core | OAuth config validation error. |
| `PkceChallenge` | struct | core | PKCE S256 challenge + verifier. |
| `OAuthState` | struct | core | CSRF state parameter. |
| `AuthorizationRequest` | struct | core | OAuth authorization URL builder. |
| `TokenExchangeRequest` | struct | core | OAuth code-for-token exchange. |
| `TokenResponse` | struct | core | Parsed token endpoint response. |
| `TokenResponseError` | struct | core | Token response parse error. |
| `RefreshRequest` | struct | core | OAuth refresh token request. |
| `OidcDiscovery` | struct | `oidc` | OpenID Connect discovery document. |
| `OidcDiscoveryError` | struct | `oidc` | Discovery document parse error. |
| `IdTokenClaims` | struct | `oidc` | ID token claims. |
| `IdTokenClaimsError` | struct | `oidc` | ID token claims error. |
| `IdTokenValidator` | struct | `oidc` | ID token validator. |
| `IdTokenError` | struct | `oidc` | ID token validation error. |
| `UserInfo` | struct | `oidc` | UserInfo endpoint response. |
| `UserInfoError` | struct | `oidc` | UserInfo parse error. |
| `SamlConfig` | struct | `saml` | SAML SP configuration. |
| `SamlResponse` | struct | `saml` | Parsed SAML response. |
| `SamlResponseError` | struct | `saml` | SAML response parse error. |
| `SamlStatus` | enum | `saml` | SAML status code. |
| `SamlAssertion` | struct | `saml` | SAML assertion. |
| `SamlSubject` | struct | `saml` | SAML subject (NameID). |
| `SamlConditions` | struct | `saml` | SAML conditions (audience, time). |
| `SamlAttribute` | struct | `saml` | SAML attribute name/value. |
| `generate_sp_metadata` | fn | `saml` | Generate SP metadata XML. |
| `XmlElement` | struct | `saml` | Parsed XML element. |
| `XmlParseError` | enum | `saml` | XML parse error. |
| `parse_xml` | fn | `saml` | Parse an XML document. |
| `TotpSecret` | struct | core | TOTP shared secret (RFC 6238). |
| `TotpConfig` | struct | core | TOTP parameters (digits, period, algorithm). |
| `TotpAlgorithm` | enum | core | TOTP HMAC algorithm (SHA-1/256/512). |
| `TotpError` | struct | core | TOTP error. |
| `totp_generate` | fn | core | Generate the current TOTP code. |
| `totp_verify` | fn | core | Verify a TOTP code within a skew window. |
| `totp_verify_step` | fn | core | Verify and return the matched step (replay guarding). |
| `generate_recovery_codes` | fn | core | Generate zeroizing MFA recovery codes. |
| `HibpPrefix` | struct | core | Have-I-Been-Pwned k-anonymity SHA-1 prefix. |
| `HibpResponse` | struct | core | Parsed HIBP range response. |
| `BreachResult` | enum | core | HIBP breach lookup result. |
| `HibpError` | struct | core | HIBP error. |
| `hibp_sha1_hex` | fn | core | SHA-1 hex of a password for HIBP range queries. |
| `RelyingParty` | struct | core | WebAuthn relying party / ceremony entry point. |
| `RelyingPartyBuilder` | struct | core | Builder for `RelyingParty`. |
| `Passkey` | struct | core | A registered passkey credential. |
| `RegistrationCeremony` | struct | core | WebAuthn registration ceremony. |
| `AuthenticationCeremony` | struct | core | WebAuthn authentication ceremony. |
| `InMemoryPasskeyStore` | struct | core | In-memory passkey store. |
| `PrfSalt` | struct | core | WebAuthn PRF extension salt. |
| `PrfClientResult` | enum | core | Whether the client honored the PRF extension. |
| `WebAuthnError` | struct | core | WebAuthn error (opaque over the wire). |
| `SshPublicKey` | struct | `ssh-keys` | Parsed SSH public key (fingerprint/classification). |
| `SshKeyAlgorithm` | enum | `ssh-keys` | SSH key algorithm. |
| `SshKeyError` | struct | `ssh-keys` | SSH key parse error. |
| `AuthProviderKind` | enum | core | Authentication provider discriminant. |
| `AuthIdentity` | struct | core | Authenticated identity from any provider. |
| `AuthResult` | struct | core | Unified authentication result. |
| `AuthError` | struct | core | Top-level error type. |

Full API docs: `cargo doc --all-features --open`

## Development

```bash
git clone git@gitlab.com:entropysoftworks/crates/auth.git
cd auth
cargo test --all-features
cargo clippy --all-features -- -D warnings -W clippy::pedantic
cargo doc --all-features --no-deps
```

## License

MIT -- see [LICENSE](LICENSE).