fix(jose): report the verified signer kid from the unprotected JWS header (#10)#11
Merged
Merged
Conversation
…ader (#10) JwsParser resolves the verifying key from the per-signature unprotected header `kid` and verifies the signature against it, but previously returned `JwsParseResult.SignerKid == ""` whenever the integrity-protected header carried no `kid` — discarding the very identity the signature proved. DIDComm Messaging v2.1 places the signer `kid` in the unprotected JWS header, so signed and authcrypt(sign(...)) envelopes lost their signer identity on unpack (the five Appendix C.2/C.3 interop vectors). SignerKid is now the `kid` that resolved the key under which the signature verified: the protected header is preferred when present, otherwise the unprotected `kid` is reported. This is sound because verification has already succeeded — a rewritten unprotected `kid` resolves a different key under which the signature cannot verify, so a forged `kid` never reaches the result. Corrects the over-conservative issue #6 hardening decision (Finding #2). Unchanged: a protected-header `kid` is reported as before; a JWS whose protected and unprotected `kid` disagree is still rejected. Verified by a full Jose suite (313/313) and an adversarial review (7 attacks, none exploitable). Bumps the release line to v1.0.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
The fix and its security reasoning are solid. One issue needs to be corrected before merging: Bug: Duplicate
|
A rejected-then-reapplied edit during authoring left two identical ## [1.0.1] entries back-to-back. Keep one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
Addressed in 6601eef — removed the duplicate No code changes; the fix, tests, and version bump are unchanged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #10. Bumps the release line to v1.0.1.
Problem
JwsParserresolves the verifying key from the per-signature unprotected headerkidand verifies the signature against it, but previously returnedJwsParseResult.SignerKid == ""whenever the integrity-protected header carried nokid— discarding the very identity the signature proved.DIDComm Messaging v2.1 places the signer
kidin the unprotected JWS header, so every signed envelope and everyauthcrypt(sign(...))composition lost its signer identity on unpack. This is the only gap behind the five failing Appendix C.2/C.3 interop vectors (c2-eddsa-signed,c2-es256-signed,c2-es256k-signed,c3-authcrypt-p256,c3-authcrypt-p521); the JWE/KDF/apu/apv/1PU byte-equivalence was already correct.Fix
SignerKidis now thekidthat resolved the key under which the signature verified — the protected header is preferred when present, otherwise the unprotectedkidis reported:This is sound because verification has already succeeded: an attacker who rewrites the unprotected
kidtoK'makes the parser resolveK''s key, under which the original signature cannot verify (they don't holdK''s private key), so a forgedkidnever reaches the result. This corrects the over-conservative issue #6 hardening decision (Finding #2), whose regression test is updated here.Unchanged behavior: a
kidin the protected header is reported exactly as before, and a JWS whose protected and unprotectedkiddisagree is still rejected (MalformedJoseException). The compact path (no unprotected header) is unaffected. No public API members were added.Tests
JwsJson_KidOnlyInUnprotectedHeader_VerifiesAndReportsTheResolvedSignerKid.JwsJson_ConflictingProtectedAndUnprotectedKid_IsRejected(agreement check still fires).JwsGeneral_KidOnlyInUnprotectedHeader_VerifiesAndReportsTheResolvedSignerKid(signatures-array shape used by DIDComm multi-recipient / authcrypt).Adversarial review (AGENTS.md §2)
A dedicated adversarial subagent attempted 7 concrete attacks (kid aliasing, multi-sig forged victim kid, empty/duplicate/case/whitespace kid variants, compact path) — verdict: NOT EXPLOITABLE. Every attack is stopped by the protected-vs-unprotected agreement check (reachable in both flattened and general-array paths), post-verification ordering, strict JSON (
AllowDuplicateProperties = false),Ordinalcomparison, and the fail-closed catch blocks. The one finding (a kid-ignoring resolver) is pre-existing, not exploitable for the DIDComm consumer (whose production resolver returns a key only whenkey.Kidequals the requested kid), and left out of scope to keep this fix minimal.Acceptance criteria
kidonly in the unprotected header →SignerKidequals that kid.kidin the protected header → unchanged.kid→ still rejected.Release
v1.0.1publishes when thev1.0.1git tag is pushed (publish.yml, gated environment). Not done in this PR — tagging is the explicit release trigger.🤖 Generated with Claude Code