fix(jose): adversarial hardening pass — close #6#9
Conversation
Issue #6 asked for JWS/JWE building blocks; the JOSE layer was already implemented and passing, so this is a deep adversarial security/correctness hardening pass (9 review dimensions, each finding majority-vote verified). 5 confirmed findings fixed, each with a regression test (Hardening/HardeningRegressionTests.cs): - SD-JWT reconstruction is now depth-bounded and fails closed on a deep recursive-disclosure chain instead of overflowing the stack and crashing the host (uncatchable StackOverflowException) — the strongest fail-closed violation. (HIGH) - A malformed/unsupported cnf/issuer key now fails closed as MalformedJoseException in CompactJwt.Verify instead of escaping as an uncaught NotSupportedException/crypto exception. (MEDIUM) - JWS reports SignerKid only from the integrity-protected header; an unprotected-only kid is a routing hint, never the verified identity. (LOW) - JwtValidationOptions.ExpectedType enforces RFC 8725 §3.11 explicit typing (opt-in); JwsParseResult.Typ surfaces the protected typ. (LOW) - Base64Url.Decode is strict (rejects whitespace, padding, +/). (LOW) New opt-in public API (ExpectedType, JwsParseResult.Typ) documented and sample-covered. Full solution: 781 tests pass, build 0/0; AC-6/AC-7/AC-9 gates green. Closes #6 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
moisesja
left a comment
There was a problem hiding this comment.
Automated review — PR #9 fix(jose): adversarial hardening pass — close #6
Solid hardening pass. The methodology (9 dimensions → 3 independent skeptics → majority-vote) is rigorous, each confirmed finding is paired with a regression test, the API additions are opt-in and non-breaking, and the PR description doubles as a useful security-audit trail. A few minor observations:
Base64Url.Decode — behavioral breaking change for lenient callers
The strict alphabet loop is the correct fix for JOSE contexts, but it changes the runtime behavior of an existing public API: any out-of-repo caller that previously passed =-padded or whitespace-containing input now gets a FormatException with no compile-time warning. The CHANGELOG frames this correctly as a correctness fix, but it is worth calling out explicitly so upgraders know to scan their own Base64Url call sites. A one-line note in the CHANGELOG ### Changed section (in addition to ### Security) would make the upgrade path clear.
Off-by-one on the depth guard (cosmetic)
private const int MaxReconstructionDepth = 64;
// …
if (depth > MaxReconstructionDepth) // fires at depth 65 — allows 65 frames, not 64The constant says 64 and the comment references the JsonNode.Parse default MaxDepth = 64, but > instead of >= means 65 frames are actually permitted. No real-world SD-JWT comes near either number and the test uses 200 links, so this is purely cosmetic. If you ever want the semantics to match the name exactly, swap to >= MaxReconstructionDepth.
samples-coverage-report.md contains an absolute local path
Samples directory: `/Users/moises/Projects/dataproofs-dotnet/samples`
Not a security issue, but the committed report will show a stale path whenever it is regenerated in CI or on another machine. Minor — worth addressing the next time the report is regenerated from a portable root.
Everything else looks correct
CompactJwtexception filter correctly passes throughMalformedJoseExceptionandOperationCanceledExceptionbefore wrapping anything else.TypeMatchescorrectly normalises theapplication/prefix (case-insensitive, per RFC 8725 §3.11).SignerKidnow sourced fromheader.Kid(integrity-protected) rather thansig.Kid(unprotected) — closes the real identity-confusion risk.- 781/0 test result and samples-coverage gate green are good final confirmators.
None of the above block merge — the security fixes are sound.
Generated by Claude Code
The ac-5 gate pinned the ported JWS/JWE tests to a didcomm-dotnet source commit SHA and diffed assertions against it. With didcomm-dotnet being refactored, that pin is no longer meaningful, so the gate is removed: - Drop the ac-5 job from .github/workflows/ci.yml (and its now-stale cross-references in the ac-3 comment and header). - Delete the tasks/parity-diff tool (removed from the solution) and the tests/.../Parity/PARITY.md manifest. - Mark AC-5 retired in the PRD (§9) and adjust the Phase B exit note. The ported parity TEST suite under tests/DataProofsDotnet.Jose.Tests/Parity/ is KEPT — it is real JWS/JWE/JWK/KDF/AEAD coverage and continues to run as part of the standard Jose test suite (ac-3 and build-test). JOSE conformance is guaranteed by AC-3 (RFC vectors + oracle cross-verification). Full solution: build 0/0, 792 tests pass (no test removed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Also folded in (per discussion): retired the Root cause of the But since didcomm-dotnet is being refactored, the source-diff pin is becoming meaningless, so rather than fix the tool we removed the gate:
Kept the ported parity test suite ( Full solution: build 0/0, 792 tests pass (no test removed). |
- CHANGELOG: add a ### Changed note that Base64Url.Decode is now strict (padded/whitespace input now throws FormatException) so upgraders scan their call sites — in addition to the existing Security entry. - SdJwtReconstructor: depth guard uses `>=` so the permitted nesting matches the named bound exactly (MaxReconstructionDepth = 64 → depths 0..63). - samples-coverage: emit repo-relative paths in the report so the committed artifact is machine-independent (no absolute local path); regenerated. Full solution: build 0/0, 792 tests pass; samples-coverage gate green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-existing CI failures: - Windows build+test: RdfCanonicalizationConformanceTests failed because the .nq/.json fixtures were checked out as CRLF on windows-latest (core.autocrlf), inflating their byte length vs the LF N-Quads the canonicalizer emits. Add a .gitattributes pinning tests/fixtures/** (and *.nq/*.nt) to LF. All fixtures are already LF in the repo, so nothing is renormalized — this only constrains checkout. - ac-10 smoke: the step hardcoded version="0.1.0" while packages pack as 0.1.0-preview.2. Derive the version from the packed Core nupkg so the smoke install always matches what was packed. NetCrypto 1.0.0 -> 1.1.0 (requested): the library source is unaffected. Compat fixes for the two breaking changes 1.1.0 introduces: - IKeyStore gained DeriveSharedSecretAsync(...) — implement it in the test RecordingKeyStore (delegates to the inner store, records the call). - NetCrypto now ships a Base64Url type — add a `using Base64Url = DataProofsDotnet.Jose.Base64Url;` alias to the 6 samples that import both namespaces, resolving the ambiguity. Full solution: build 0/0, 792 tests pass; samples-coverage green; all samples run to exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Closes #6. Issue #6 asked for JWS/JWE enveloping building blocks; investigation found the JOSE layer (
DataProofsDotnet.Jose: JWS/JWE/JWT/JWK/SD-JWT/SD-JWT VC/VC-JOSE) already implemented and passing — the issue was stale. Per the chosen path, this is a deep adversarial security/correctness hardening pass of that surface, fixing every confirmed weakness and pinning each with a regression test.Method
A multi-agent workflow reviewed 9 dimensions (JWS bypass, JWE key-mgmt, ECDH-1PU, multi-recipient, JWK, SD-JWT/KB-JWT/VC, JWT, encoding/JSON, fail-closed/hygiene). Every finding was then put through 3 independent refute-by-default skeptics (exploitability / spec-correctness / already-mitigated lenses) and kept only on a ≥2/3 majority. Result: 11 findings verified, 5 confirmed, 6 refuted (correctly — by-design or already-guarded).
Confirmed findings fixed (each with a regression test)
SdJwtReconstructorwalked the disclosed payload by unbounded recursion; a chained recursive-disclosure presentation (RFC 9901 §6.3) rooted in an issuer-signed_sddigest could drive an uncatchableStackOverflowExceptionthat terminates the host process — the strongest fail-closed violation.MalformedJoseException(DISCLOSURE_INVALID).cnf(or issuer) key madeCompactJwt.Verifyescape as an uncaughtNotSupportedException/crypto exception (reachable via an attacker-influencedcnfon the KB path).MalformedJoseException, which both call sites already handle.SignerKidfrom the unauthenticated unprotected header.kidstays a resolution hint.typ(RFC 8725 §3.11 token confusion).JwtValidationOptions.ExpectedType(case-insensitive,application/-prefix tolerant) + surfacedJwsParseResult.Typ.Base64Url.Decodeaccepted whitespace/padding, contradicting the no-pad contract.Refuted (no change, with reasons): un-zeroed sender key on a 1PU build-throw, A256KW recipient-set commitment (by design),
vct#integritynot validated (optional), padding-only base64 (subsumed by the strict fix), deterministic-number canonicalization (headers carry no floats).API additions (opt-in, non-breaking)
JwtValidationOptions.ExpectedType— pin the JWStyponJwtHandler.Verify.JwsParseResult.Typ— the integrity-protectedtypheader.Both are in
PublicAPI.Unshipped.txtand exercised bySamples.JwtAndJwk(AC-9 gate green).Verification
dotnet build DataProofsDotnet.sln→ 0 warnings, 0 errors.dotnet test DataProofsDotnet.sln→ 781 passed, 0 failed (Jose 311, +14 new hardening regressions; no regressions elsewhere).samples-coveragegate green (530 members across 6 packages, 0 uncovered); AC-6 hygiene + AC-7 API discipline green.🤖 Generated with Claude Code