fix(oauth-client): correct RP crypto, clean up sign_dpop errors, tighten happy-path test
Three groups of changes that only make sense together, because tightening
the happy-path assertions exposes crypto bugs that must be fixed in the
same commit for bisect-ability:
**Crypto correctness (relying_party.rs):**
1. new_pkce was computing the PKCE S256 code_challenge as
BASE64URL(SHA256(raw_random_bytes)). RFC 7636 §4.2 requires
BASE64URL(SHA256(ASCII(code_verifier))) — the hash is over the ASCII
of the base64url-encoded verifier string, not over the raw preimage.
Fix: hasher.update(verifier.as_bytes()). The fake AS already matched
the RFC, so this was a pure RP-side bug.
2. sign_dpop was base64url-encoding the DPoP signature a second time.
jsonwebtoken::crypto::sign returns a base64url-encoded String, not
raw bytes. The code treated it as Vec<u8> and re-encoded it, producing
a nonsense signature that verification rejected with
invalid_dpop_proof. Fix: take the returned String directly into the
JWS.
**Interactive stage (pipeline/interactive.rs):**
3. The DPoP header presence check compared against the literal "DPoP",
but axum stores header names lowercase in HeaderMap::as_str. Fix:
eq_ignore_ascii_case.
**Cleanup (relying_party.rs, addresses cycle-3 I3 and M1):**
4. sign_dpop's two serde_json::to_vec call sites now use
.expect("serde_json::to_vec over Value is infallible") instead of
mapping to RpError::MetadataMalformed. serde_json::to_vec over a
serde_json::Value is infallible in practice, and MetadataMalformed
was semantically wrong for a serialization failure.
5. The dead signing_key: Arc<P256SigningKey> field and its
#[expect(dead_code)] are removed. encoding_key + signing_jwk_public
are both derived at construction, so signing_key is now a local in
new() that's dropped after use.
**Test tightening (tests/oauth_client_interactive.rs, addresses cycle-3 C2):**
6. interactive_happy_path_gates_all_pass now asserts CheckStatus::Pass
by name on each of ClientReachedPar, ClientUsedPkceS256,
ClientIncludedDpop, ClientCompletedToken. The previous assertion
set (len == 6 + ServerBound + ClientRefreshed) was satisfied even
when every path-variable check was SpecViolation. The tight
assertions exercise the real RP↔fake-AS round-trip and caught the
crypto bugs above.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>