domain: split-token base64 and constant-time verify duplicated across three credential types #65
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier#65
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during a code review of the
domaincrate.Location:
crates/domain/src/models/access_token.rs:237,authorization_code.rs:315,user.rs:337(parse/Display);access_token.rs:319,authorization_code.rs:427,user.rs:474(ct_eq verify)Type: Cleanup — duplication
Problem
The 32-byte base64url split-token encode/decode ([0..=15]/[16..=31]) parse and Display are copy-pasted verbatim across AccessToken, AuthorizationCode, and ConfirmationToken, and the constant-time
subtle::ct_eqverify block is likewise triplicated.credential.rsdocuments itself as the place this logic is written once, yet only generate/parse/hash live there.Cost
A change to the wire format or the comparison must be edited in three parse fns, three Display impls, and three verify paths, with copy-pasted error strings; a fourth credential family will copy it again.
Suggested fix
Add
encode(&Identifier, &Verifier) -> String,decode(SecretString) -> Result<(Identifier, Verifier)>, andVerifierHash::matches(&VerifierHash) -> booltocredential.rsand have all three wrappers delegate.Agent Brief
Category: enhancement
Summary: Move the split-token base64 wire format and constant-time verifier comparison into the shared credential module, and have all three credential families delegate
Current behavior:
The shared credential module (
credential.rsin the domain crate's models) definesIdentifier,Verifier, andVerifierHashand its module doc promises the common logic is "written once" there — but onlygenerate,parse, andhashactually are. Three pieces are copy-pasted across theAccessToken,AuthorizationCode, andConfirmationTokenfamilies instead:Displayimpls): concatenate identifier bytes + verifier bytes and base64-encode with the padded URL-safe alphabet. Verbatim triplicate.parseon the minted-credential types, taking aSecretString): base64-decode, ensure exactly 32 bytes, split into[0..=15]identifier and[16..=31]verifier. Near-verbatim triplicate with copy-pasted error strings differing only in the credential noun.AccessTokenRecord::verify,AuthorizationCodeRecord's redemption checks, andConfirmation's verify methods): asubtle::ct_eqof the storedVerifierHashagainst the presented verifier's hash. Verbatim triplicate; only the returned error variant differs.Any change to the wire format or the comparison must be made in three parse functions, three
Displayimpls, and three verify paths; a fourth credential family would copy all of it again.Desired behavior:
The credential module is the single home for this logic, as its doc already claims:
encode(&Identifier, &Verifier) -> String— the split-token wire format (id bytes + verifier bytes, padded URL-safe base64).decode(SecretString) -> Result<(Identifier, Verifier), anyhow::Error>— the inverse: decode, length-check, split.VerifierHash::matches(&VerifierHash) -> bool— the constant-time comparison, keepingsubtle::ct_eqinside the shared module.The three families'
Display,parse, and verify paths delegate to these. Per-family error variants (e.g.VerifierMismatchon each family's error enum) and per-family error context (which credential failed to decode) stay at the wrapper layer — the shared module stays credential-agnostic. Follow the glossary: these types are the minted form of a credential (CONTEXT.md, Credentials section).Key interfaces:
Identifier/Verifier/VerifierHashwith the samepub(crate)visibility the module already uses.AccessToken/AuthorizationCode/ConfirmationTokenparse+Display, and the verifier checks inAccessTokenRecord::verify,AuthorizationCodeRecord's redemption, andConfirmation's verify methods.token_roundtrips_through_base64,code_roundtrips_through_base64,confirmation_token_encode_decode_test) and the verifier-mismatch tests — they should pass unmodified.Acceptance criteria:
encode,decode, andVerifierHash::matchesexist in the shared credential module; the comparison insidematchesis constant-time (subtle)Display,parse, and verifier checks delegate — no base64 engine use orct_eqcall remains in the three credential families for the split-token format (the PKCE challenge comparison is separate and stays)cimise task passesOut of scope:
CodeChallengecomparison in the authorization-code redemption — different shape (string compare of a base64-encoded hash), not part of the triplicationAccessTokenIdvsAuthorizationCodeIdetc.) or their error enums — the type-safety split is deliberateto_url_safe/from_url_safe) — different format (no-pad, identifier only), different purpose (routes)