Skip to content

feat(settings): Add passkey HPKE key-wrapping crypto lib - #21006

Draft
nshirley wants to merge 1 commit into
mainfrom
FXA-13144
Draft

feat(settings): Add passkey HPKE key-wrapping crypto lib#21006
nshirley wants to merge 1 commit into
mainfrom
FXA-13144

Conversation

@nshirley

Copy link
Copy Markdown
Contributor

Because

  • Passkey PRF-derived keys need an HPKE envelope so account keys can be wrapped
    client-side, which is the foundation the passkey sync sign-in flow builds on.
  • The envelope format is frozen once the first wrap is written in production —
    there is no version field, so the primitives and their sizes need to be
    settled and test-pinned before anything composes them.

This pull request

  • Adds packages/fxa-settings/src/lib/passkey-crypto/, a client-only module in
    two layers: hpke.ts seals and opens kB against a per-wrap P-521 recipient
    key, and key-wrap.ts generates that keypair and wraps its private scalar
    under the WebAuthn PRF output with AES-256-GCM.
  • Holds the shared CipherSuite in suite.ts so the AES-GCM layer can use
    SerializePrivateKey without depending on the HPKE layer above it, and
    freezes the wire sizes in constants.ts.
  • Stores skR as the RFC 9180 raw scalar rather than a PKCS#8 export, making
    prfWrappedSkR 82 bytes instead of 257. Nsk = 66 is a ciphersuite constant,
    whereas PKCS#8's 241 bytes depend on Web Crypto emitting an OPTIONAL
    public-key field no spec requires — an implementation that omitted it would be
    zero-padded into the fixed-width column and never open again.
  • Requires pkR alongside the private key in openKb, so the library never
    derives the public key via crypto.subtle.getPublicKey, whose fallback needs
    an extractable key and fails only in browsers lacking that method.
  • Adds hpke@1.1.3 as an exact pin and allows it through Jest's
    transformIgnorePatterns.
  • Covers the module with 41 tests against real Web Crypto and the real library,
    including the RFC 9180 Appendix A.6 vectors.

Issue that this pull request solves

Closes: FXA-13144

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: constants.ts (the frozen sizes), and the
    skR-as-raw-scalar reasoning in V1_SIZES.skRRaw.
  • Suggested review order: README.mdconstants.tssuite.ts
    key-wrap.tshpke.ts → tests.
  • Risky or complex parts: every size here is permanent once wraps exist in
    production. The two constraints most easily undone are the fresh-per-wrap
    keyWrapIv and openKb taking pkR; both have dedicated regression tests.

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

Depends on #20958 (FXA-13138), which must merge first. V1_SIZES.prfWrappedSkR = 82
here and prfWrappedSkR BINARY(82) in patch-194-195.sql there are the same decision in
two places and have to land together — merging this first would leave constants on main
describing a column that does not exist yet.

Nothing imports this module yet. FXA-13147 composes these primitives into the create and
unwrap flows, so this PR adds no behaviour on its own.

Because:
* Passkey PRF-derived keys need an HPKE envelope to wrap account keys
  client-side.

This commit:
* Adds packages/fxa-settings/src/lib/passkey-crypto with HPKE seal/open
  and key-wrap helpers, the shared ciphersuite in suite.ts, RFC 9180 A.6
  test vectors, and unit tests.
* Stores skR as the raw scalar (Nsk 66) rather than a pkcs8 export, so
  every stored length is a ciphersuite constant. pkcs8's 241 bytes rest
  on Web Crypto emitting an OPTIONAL public-key field, and a conforming
  implementation that omitted it would be zero-padded into the
  fixed-width column and never open again. Serialising is left to the
  library, which pads short scalars: ~1 P-521 key in 512 has a leading
  zero byte, so a stripping implementation would corrupt those wraps
  undetectably. Covered by a regression test.
* Keeps openKb taking pkR alongside the private key, so the library never
  derives it via crypto.subtle.getPublicKey, whose fallback needs an
  extractable key and fails only in browsers lacking the method.
* Adds the hpke dependency and allows it through Jest's
  transformIgnorePatterns.
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