Portable identity passkey PRF and canonical RP ceremony - #360
Draft
NotASithLord wants to merge 6 commits into
Draft
Portable identity passkey PRF and canonical RP ceremony#360NotASithLord wants to merge 6 commits into
NotASithLord wants to merge 6 commits into
Conversation
Visual regression✅ No visual drift. Every state renders identically to the committed baselines. 📸 Browse the visual gallery — 50 baseline screens (light + dark) as of this commit. |
…PRF wrappers and the canonical-RP ceremony Before 1.0, every constant a portable passkey binds to permanently must be decided and locked. This lands all of them: - RP decided (docs/design/portable-identity/04): RP ID peerd.ai, the ceremony origin id.peerd.ai. The origin may move within peerd.ai until the first production mint; the RP ID may not. - passkey-prf wrapper kind (credential-wrapper.js): HKDF over the authenticator's PRF output feeding AES-KW around CapK. No KDF descriptor, so an untrusted record has no work-factor knobs. Bounded credentialId and transports metadata; record build/open/adopt accept prfOutput beside the passphrase. Unlike the passphrase wrapper, a stolen record gives no offline oracle against this kind: the KEK requires the authenticator. - Frozen derivation vectors (tests/peerd-distributed/ identity-prf-vectors.test.ts): the PRF input digest and a known-answer wrap lock the input tag, zero HKDF salt, info string, and RFC 3394 wrap in CI. A failing vector means credentials would be orphaned: revert the derivation, never the vector. - identity/handoff.js, the ceremony handoff. The id.peerd.ai page is a pure PRF oracle: the request rides a URL fragment out (ephemeral P-256 key plus challenge), the PRF output rides a fragment back sealed AEAD to that key (ECDH, then HKDF with the challenge as salt, then AES-GCM). The page never sees a seed, capsule, record, or CapK; fragments never reach a server; the history residue is single-request ciphertext. Import-free on purpose: web-identity/handoff.js is a byte-identical copy and CI fails on drift. - web-identity/: the static, dependency-free ceremony page source (register/get flows, frozen PRF input, localhost dev RP), vendored by the site repo per its README. Still ahead (doc 04): deploy the page, grow the backup/restore UI's passkey path over the exported handoff surface, live cross-browser ceremony tests, then the first real mint freezes the origin too.
NotASithLord
force-pushed
the
claude/peerd-cross-origin-identity-x7f662
branch
from
August 8, 2026 21:30
efd11df to
e9aa89c
Compare
Verified findings from an adversarial multi-reviewer pass over the diff: - web-identity/ escaped every gate (medium): // @ts-check was inert, the dir was unlinted and unchecked. Add web-identity to the lint scope (package.json) and the tsconfig include; the now-live typecheck caught a missing BufferSource cast on the PRF input, now fixed. - Threat-model prose overstated the sealing (low): a compromised RP page reads the PRF output in PLAINTEXT (it must, to seal it), and because the PRF input is a frozen constant that output is the credential's PERMANENT wrapper-KEK source, not 'ciphertext bound to one request'. The AEAD protects only the return leg from off-page observers. Corrected in identity-rp.js, docs 04, and web-identity/README - and stated as the reason page compromise forces credential re-enrollment. - Unguarded decodeURIComponent (low): parseCeremonyRequest and extractSealedResponse threw a bare URIError on a malformed percent sequence in a fragment we do not control, breaking the module's typed- error taxonomy and the null-on-garbage contract. Add a guarded decodePercent: typed bad-envelope on the request path, null on the response path. - Unvalidated field spread (low): parseCeremonyRequest spread the untrusted fragment into the parsed request. Rebuild from validated fields only (v, flow, challenge, epk stripped to kty/crv/x/y, bounded credentialId + transports). - Partial zeroization (low): wipe the raw ECDH shared secret after it enters the non-extractable HKDF handle; comment the honest residual (base64-in-JSON string copies of the PRF output cannot be wiped). - Dead dark-mode CSS (low): the @media block preceded the base rules at equal specificity, so the overrides never applied. Move it after, add an .err dark variant, comment the ordering rule. handoff.js and its byte-identical web-identity/ copy stay in sync (the copy-equality test still passes). Refuted findings (forged/unauthenticated response, residentKey downgrade, oracle framing) were checked against the code and left as-is.
…igin-identity-x7f662 # Conflicts: # packaging/check-tscheck.ts
…igin-identity-x7f662 # Conflicts: # packaging/check-tscheck.ts
…igin-identity-x7f662 # Conflicts: # packaging/check-tscheck.ts
The essential integration guarantee for portable identity: a recovered did must not just verify in isolation, it must be the identity the p2p mesh authenticates as. Restore and mesh-join meet at one vault secret (distributed/identity/v1); this test drives that exact seam over a fake secret store, reproducing what offscreen/dweb-base.js does: - fresh install: adopt a recovery record, write the material to the secret (as dwebTransfer.adoptRecord does), then load it back through loadIdentityMaterial + identityFromMaterial (the mesh's own path) and prove the identity signs a HELLO-style payload verifiable under the ORIGINAL did. - a plain load does not re-mint or fork the stored identity. - replace-on-different-did yields the incoming did on the next mesh load (the runtime restart around the custody write is what makes a live mesh rejoin as the restored identity). Values-level coverage of the same wiring the live two-peer job exercises with a freshly minted identity; no production change.
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.
What changed
peerd.aiand the ceremony origin asid.peerd.aipasskey-prfcredential wrapper and strict recovery-record validationWhy
Portable identity credentials bind permanently to protocol constants. Those constants must be reviewed and fixed before the first production credential is created. Changing them later would orphan existing credentials.
The ceremony page acts only as a PRF oracle. It receives a public key and challenge in the URL fragment, then returns the PRF output encrypted to that request. It never receives the identity seed, capsule, record, or capsule key.
Still required before this can be ready
web-identity/through the site repositoryValidation on the current draft head
Review notes
This touches portable identity and the dweb identity module. It does not change service-worker routes, vault custody, transfer paths, or tool gates.
web-identity/allows localhost only for development credentials; those credentials cannot collide withpeerd.aicredentials.