Skip to content

fix: close the PPID deep-dive findings (pairwise, server, federation) - #656

Merged
osanderson merged 14 commits into
mainfrom
fix/ppid-deep-dive-a
Oct 13, 2026
Merged

osanderson merged 14 commits into
mainfrom
fix/ppid-deep-dive-a

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Closes the pairwise subject (PPID) deep-dive findings in pairwise, server, storage and federation before v0.55.0. Everything changed here is new in 0.55, so nothing breaks a released API. The resource/serverresource findings are in #655.

API changes (all new in 0.55)

  • SubjectConfig.Derivation, pairwise.Derivation, pairwise.OneWay and pairwise.Reversible are removed. A deployment's subs can be mapped back exactly when its deriver implements pairwise.Lookup (or a PairwiseSubjectResolver is declared), so the configuration can't claim more unlinkability than the deriver gives. A resolver alongside a reversible deriver is refused.
  • The constructors return concrete types: *pairwise.HMACDeriver, *pairwise.ReversibleDeriver, *pairwise.MappingDeriver, each with KeyCustody(). Under production assurance, an undeclared custody fails with "must declare Durable".
  • New pairwise.NewHMACWith(HMACOps, ...): the one-way deriver over a key held in an HSM or KMS, with independently generated test vectors.
  • AutomaticRegistration.SubjectTypes left empty under a pairwise default now offers federation Relying Parties pairwise subs only.
  • Server.EntityConfiguration now sets subject_types_supported in openid_provider to AutomaticRegistrationSubjectTypes() when automatic registration is configured, on a copy of the caller's map.

Fixes

  • H1: reversibility is inferred from the deriver, as above.
  • H2: New probes the deriver across two sectors and two users. It checks that derivation is deterministic, that the sub varies by sector and by user, and that no internal subject appears in the body.
  • Security M1: a client that may authenticate by attestation gets no derived sector, as CIBA clients already don't. Its tokens arrive without a redirect proving the host is its own.
  • Security L1: ReversibleOps output is copied before it's authenticated. The interface is documented as "don't retain or modify inputs".
  • M2: federation Relying Parties get pairwise subs only under a pairwise default.
  • L3: an explicit Sector must be a host name, and is lowercased.
  • Keys and prefix: a key made entirely of printable text (a passphrase, or a still-encoded key) is refused. An empty prefix is refused for one-way subs with no resolver.
  • Conformance L1: grants record the prefix they were issued under. A grant issued under another prefix is refused with invalid_grant at refresh, code exchange or CIBA token exchange, since resource servers resolve subs under the current prefix.
  • Conformance L2: SignUserInfoResponse no longer requires a prefixed sub to resolve under the client's current sector, so a grant's signed UserInfo keeps working after a sector change. The lookup still runs when the prefix is empty.
  • M7/I5: a client whose sub couldn't be issued is refused before its user signs in, at PAR and at the backchannel authentication endpoint. That covers a public client under PairwiseOnly, and a pairwise client with no sector or no deriver.
  • Federation subject types are advertised in the Entity Configuration automatically, so neither in-repo builder needed a change.

Docs

  • M4: the resolver contract.
  • I1: OIDC Core §8.1 boundary: only the OP and its own resource servers may reverse a sub.
  • Sector: set by the operator only.
  • CIBA: the CIBA Core pairwise-sector deviation.
  • Federation: the Entity-host sector is stricter than §8.1; path tenants on one host share a sector; a sector_identifier_uri added by a superior's policy isn't supported.
  • IDTokenClaims/GrantID: neither may re-identify the user.
  • ARCHITECTURE: a new subject-flow section.
  • GETTING_STARTED: the pairwise example now uses SubjectTypesPairwiseOnly; plus the one-way HSM/KMS material.
  • UPGRADING: v0.55.0 updated.
  • Doc files updated:
    • ARCHITECTURE.md
    • GETTING_STARTED.md
    • UPGRADING.md
    • docs/guides/openid-federation.md
    • conformance/server/oidf-config/README.md
    • Go doc comments in pairwise, server, storage and federation

Testing

  • Regression tests for each finding; mutation-checked.
  • Go checks:
    • go vet;
    • go test on Go 1.27 and 1.26.9;
    • golangci-lint (0 issues);
    • linux/arm and 386 vet;
    • vet and -race tests for the examples;
    • the conformance-as pairwise smoke test.
  • gocognit: no newly over-limit function.

🤖 Generated with Claude Code

osanderson and others added 14 commits October 13, 2026 14:00
LookupInternalSubject authenticated the plaintext EncryptCTR returned,
then parsed that same slice. An implementation reusing its output
buffer (a pooled HSM or KMS client) could have it overwritten by a
concurrent lookup in between, resolving one user's body to another
user. The result is now copied, and ReversibleOps documents that
implementations must not retain or modify their inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SubjectConfig.Derivation let a configuration label a reversible deriver
(NewReversible, NewMapping) one-way, so it claimed unlinkability the
deriver didn't give, and only the opposite mismatch was refused.
Derivation and its pairwise.OneWay and pairwise.Reversible constants
are gone: a deployment's pairwise subs are reversible exactly when
Dependencies.PairwiseSubjects implements pairwise.Lookup.

The built-in constructors now return concrete types, *HMACDeriver,
*ReversibleDeriver and *MappingDeriver, so a reversible deriver can be
given to a resource server as a pairwise.Lookup without a type
assertion. Each reports its key custody through KeyCustody, the
declared one or none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NewHMAC and NewReversible accepted any 32 bytes, including a
passphrase or a hex or base64 key passed without decoding. A key made
entirely of printable text is now refused: 32 random bytes are all
text with probability about 4e-14. The published reversible vectors,
whose keys are sequential bytes, use an unchecked test constructor.

server.New refuses an empty subjects.prefix when pairwise_subjects is
one-way and no resolver is set: nothing could then tell a pairwise sub
from an internal one, and the signed UserInfo check would sign the
internal subject for a pairwise client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NewHMACWith builds the one-way deriver over HMACOps, a single
HMAC-SHA-256 operation an HSM or KMS performs with a key it never
releases, producing exactly NewHMAC's bodies for the same key. Unlike
the reversible construction it needs no raw AES-CTR, so AWS KMS, Google
Cloud KMS, Azure Managed HSM, Vault Transit and PKCS#11 HSMs can all
hold the key. NewHMAC now runs on the same path. The MAC is checked to
be 32 bytes, and failures fail the derivation.

The independent generator for the HMAC test vectors is committed beside
them, and GETTING_STARTED documents the per-service options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A set Sector was used verbatim, so "RP.example" and the "rp.example" a
client derives from its redirect URIs were different sectors, and a URL
such as "https://rp.example/x" was accepted as one, though OpenID
Connect Core §8.1's sector is a host. A set Sector must now be a host
name (letters, digits, '-', '.', '_', no empty label) and is
lowercased like a derived one.

The Sector doc also says it is the operator's to set, never a client's:
two clients sharing a sector share every user's sub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…estation

A client that may authenticate by attestation gets tokens through an
embedder's own grant (Server.IssueAccessToken and IssueRefreshToken),
with no redirect proving it controls its redirect URIs' host. Deriving
its sector from them let it name another relying party's host and
receive that relying party's pairwise subs, as CIBA clients could
before. Like a CIBA client, it now derives no sector, so a pairwise one
must set Sector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… default

With a pairwise Config.Subjects.Default and no
AutomaticRegistration.SubjectTypes, a Relying Party registering itself
through OpenID Federation could ask for public subs and receive the
internal subject, opting out of the deployment's privacy choice. An
empty AutomaticRegistration.SubjectTypes now offers pairwise only under
a pairwise default; listing public widens it explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The startup probe derived one fixed input and checked only the body's
form, so a custom Deriver that returned the internal subject, or
ignored the sector or the user, passed New. It now also derives the
same input again, the same user in a second sector and a second user
in the same sector, and refuses a deriver that isn't deterministic,
puts the internal subject in the body, or gives a user one body in two
sectors or two users one body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refreshed grant kept the pairwise sub it was issued with, but a
resource server and the signed UserInfo check strip the current
Config.Subjects Prefix to resolve a sub, so after a prefix change every
reversible grant's tokens failed there with invalid_token, contrary to
the continuity the docs promised. Each pairwise grant now records the
prefix it was issued under, and one issued under another prefix is
refused at its next use (refresh, code exchange, CIBA token exchange)
with invalid_grant, as grants issued with public subs are under
SubjectTypesPairwiseOnly. The client authorizes again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange

SignUserInfoResponse looked a pairwise client's sub up under the
client's current sector, so after a sector change it refused the sub an
existing grant was issued with, which resource servers still accept
(they use the access token's pairwise_sector). Its purpose is a
backstop against the internal subject reaching a pairwise client, and a
non-empty prefix already shows a well-formed sub isn't one, so the
sector-bound lookup now runs only with an empty prefix, where nothing
else can tell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s in

A client whose sub couldn't be issued, one registered public at a
server that issues only pairwise subs, or a pairwise one with no sector
or no deriver, passed PAR and the backchannel authentication endpoint,
and failed only at completion, after its user had logged in and
consented. Both endpoints now refuse it with server_error up front;
completion still checks, for a client changed in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ration

When Config.AutomaticRegistration narrows the subject types federation
Relying Parties may ask for, Server.Metadata still advertises
everything the server issues, and an Entity Configuration built from it
(as both in-repo builders do) told Relying Parties they could ask for
types they'd be refused. With automatic registration configured,
EntityConfiguration now sets the openid_provider object's
subject_types_supported to AutomaticRegistrationSubjectTypes itself,
on a copy of the caller's map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ARCHITECTURE: a subject-flow section (type, sector, derivation,
  continuity, outbound and inbound subs, who may reverse them).
- pairwise: only the OP and its own resource servers may hold what
  reverses a sub (OpenID Connect Core 8.1).
- storage: the CIBA pairwise sector is operator-set, departing from
  CIBA Core's jwks_uri-host sector.
- federation: the Entity-host sector is stricter than 8.1; path-scoped
  tenants on one host share a sector; a sector_identifier_uri from a
  superior's metadata policy isn't supported either.
- server: IDTokenClaims and GrantID mustn't re-identify a pairwise
  client's user.
- GETTING_STARTED: the pairwise example gives every client pairwise
  subs (SubjectTypesPairwiseOnly); the sector, early refusal and
  prefix-change notes; NewHMACWith custody.
- UPGRADING v0.55.0 and the federation guide: attestation clients, the
  prefix change, shared hosts, Entity Configuration subject types.
- Test comments no longer name the removed pairwise.OneWay/Reversible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps BeginBackchannelAuthentication within the cognitive-complexity
limit after the early pairwise-client refusal was added to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.39785% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
server/federation.go 60.00% 4 Missing and 4 partials ⚠️
server/subjects.go 85.96% 4 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit 11705aa into main Oct 13, 2026
19 checks passed
@osanderson
osanderson deleted the fix/ppid-deep-dive-a branch October 13, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant