Repository navigation
feat(pairwise): add NewReversible, a reversible pairwise deriver - #649
Merged
Merged
Conversation
NewReversible encrypts the internal subject into a pairwise sub, so the OP can map any sub it issued back to its end user without storing anything: JWT access tokens, hints and resource servers all resolve through pairwise.Lookup. It is the deriver the docs now recommend. The construction is SIV (Rogaway and Shrimpton, EUROCRYPT 2006), assembled from the standard library since Go has no AES-SIV and the module has no dependencies: a synthetic IV, HMAC-SHA-256 under one key over the length-prefixed domain, issuer and sector and the padded plaintext, truncated to 16 bytes; then AES-256-CTR under a second key from that IV. The plaintext is the subject's length, the subject and zero padding to a fixed 152 bytes, so every body is 224 characters and hides the subject's length; internal subjects are limited to 151 bytes (ErrSubjectTooLong). Lookup authenticates before reading anything: a body altered in any bit, or looked up under another issuer or sector, is ErrSubjectNotFound, never another subject. The keys are refused when they're the wrong size, a placeholder, or equal, and declare custody as NewHMAC's do. Anyone holding both keys can map every pairwise sub ever issued back to its user; the docs say so, and that they can't rotate without changing every sub. Known-answer vectors in pairwise/testdata/reversible_vectors.json come from an independent reference (reversible_vectors.py: Python's hmac/hashlib and the OpenSSL command-line tool for AES-256-CTR), and three fuzz targets cover the round trip, tampering and arbitrary input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ses one server.NewSubjectID accepts internal subjects of up to 255 bytes, but pairwise.NewReversible holds at most 151, so a longer one failed a pairwise completion with an opaque "failed to derive" server_error. The error now names the limit, and the limit is exported as pairwise.MaxReversibleSubjectLength (renamed from ReversibleMaxSubjectLength, unreleased) and documented on NewSubjectID, SubjectConfig and in GETTING_STARTED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract's tampered-body case XORed one character with 0x01, which for characters like A, z, - and _ leaves the base64url alphabet, so the case only reached a Deriver's decoding. It now rotates the character within the alphabet, so the altered body still decodes and the case reaches the Deriver's own authentication or lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No Input carries an issuer or sector longer than MaxInputLength, so no body was derived under one. Lookup now refuses them as not found before computing over them, which also keeps the construction's 4-byte length prefixes exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two independently computed vectors: a non-ASCII issuer and sector (length-prefixed as UTF-8 bytes), and a subject whose synthetic IV ends in 0xfffffffd, found by an offline search, so AES-CTR over the 10-block plaintext carries out of the IV's low 32-bit word and a port that increments only a 32-bit counter fails it. A test keeps a carrying vector in the set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Excludes pairwise/contract.go from analysis like storage/contract.go (it is a reusable contract test suite), and **/testdata/** from coverage (fixtures and the scripts that generate them); names the repeated fixture values in the vector generator, which still produces identical vectors; and inlines a compared-once test variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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.



Adds
pairwise.NewReversible, the recommended pairwise deriver: it encrypts the internal subject into the pairwisesub, so the OP can map anysubit issued back to its end user without storing anything. JWT access tokens,id_token_hint/login_hint/claimssubvalues and resource servers all resolve throughpairwise.Lookup, as withNewMapping.Construction
SIV (Rogaway and Shrimpton, "A Provable-Security Treatment of the Key-Wrap Problem", EUROCRYPT 2006), assembled from the standard library (Go has no AES-SIV; the module has no dependencies):
NewHMAC.pairwise.MaxReversibleSubjectLength,ErrSubjectTooLong); 224 characters plus a 16-character prefix stays within OIDC's 255.MaxInputLength(which noInputcarries), then decodes strictly (exact length, canonical base64url), decrypts, and authenticates the full 16-byte IV in constant time before reading the plaintext. Any altered, truncated or foreign body isErrSubjectNotFound, never another subject.The docs state plainly that anyone holding both keys can map every pairwise
subever issued back to its user, that every resource server given the keys shares that risk, and that the keys can't rotate without changing everysub. GETTING_STARTED now presentsNewReversibleas the recommended deriver, withNewMappingandNewHMACas the alternatives.Assurance
pairwise/testdata/reversible_vectors.json(11 vectors: users, sectors, issuers, a 1-byte and a 151-byte subject, a second key pair, a non-ASCII UTF-8 issuer and sector, and a subject whose IV ends in0xfffffffdso the counter carries out of its low 32-bit word, which a 32-bit-counter port fails) generated byreversible_vectors.py— Python'shmac/hashlibfor the IV and the OpenSSL CLI for AES-256-CTR. The Go code reproduces every vector in both directions.server.New→ code exchange → refresh → resource server with JWT access tokens.Review follow-ups
server.NewSubjectIDaccepts up to 255 bytes, so a longer subject than the reversible deriver holds now fails a pairwise completion with aserver_errorthat names the 151-byte limit (audited, no code issued), instead of an opaque derivation failure. The limit is exported aspairwise.MaxReversibleSubjectLengthand documented onNewSubjectID,SubjectConfigand in GETTING_STARTED.NewReversible.Non-breaking: a new constructor;
Derivation: pairwise.Reversiblealready required apairwise.Lookup.🤖 Generated with Claude Code