Skip to content

feat(webauthn): authenticatorLargeBlobs + LargeBlobStorage trait#206

Draft
AlfioEmanueleFresta wants to merge 4 commits into
masterfrom
feat/webauthn-largeblob-storage
Draft

feat(webauthn): authenticatorLargeBlobs + LargeBlobStorage trait#206
AlfioEmanueleFresta wants to merge 4 commits into
masterfrom
feat/webauthn-largeblob-storage

Conversation

@AlfioEmanueleFresta
Copy link
Copy Markdown
Member

@AlfioEmanueleFresta AlfioEmanueleFresta commented May 10, 2026

Implements the read half of the WebAuthn L3 largeBlob extension end-to-end. Stacked on top of #198 (the safe-drop fix); read this PR's diff against that branch.

Why

largeBlob lets a relying party read or write a small per-credential payload that lives on the authenticator. The CTAP 2.1 authenticatorLargeBlobs command (0x0C) exposes a single device-wide serialized array; per-credential entries within that array are encrypted under the largeBlobKey returned by authenticatorGetAssertion. Until this PR, libwebauthn could only ask the authenticator to surface a largeBlobKey; we had no way to actually fetch and decrypt the blob, so the WebAuthn unsigned_extensions_output.large_blob.blob field stayed None.

What this PR does

  • CTAP wire layer (src/proto/ctap2/model/large_blobs.rs, src/proto/ctap2/protocol.rs): adds Ctap2CommandCode::AuthenticatorLargeBlobs = 0x0C, request/response models, and a ctap2_large_blobs method on the Ctap2 trait.
  • Public LargeBlobStorage trait (src/ops/webauthn/large_blob.rs): async read / write, so callers can plug in alternative backends. Two backends ship:
    • MemoryLargeBlobStorage: in-memory HashMap, useful for tests.
    • AuthenticatorLargeBlobStorage<'_, C>: drives paginated authenticatorLargeBlobs(get), AES-256-GCM-authenticates each entry under the supplied largeBlobKey, and RFC 1951 raw-deflate-decompresses the plaintext.
  • Read-path wire-up (src/webauthn.rs): after _webauthn_get_assertion_fido2 collects assertions, if largeBlob.read = true was requested and the device returned a largeBlobKey, libwebauthn now runs the CTAP fetch-and-decrypt and populates unsigned_extensions_output.large_blob.blob. Failures here are non-fatal: per WebAuthn L3 §10.1.5 the blob field is absent when the read cannot complete.

What is deferred

The write half is intentionally a follow-up PR. It will:

  • Implement chunked authenticatorLargeBlobs(set) with pinUvAuthParam binding per CTAP 2.1 §6.10.2 / §6.10.5 (compute the SHA-256 trailer, paginate the upload, sign each chunk with the platform-derived pinUvAuthParam).
  • Wire LargeBlobStorage::write on AuthenticatorLargeBlobStorage (currently LargeBlobError::Unsupported).
  • Add the WebAuthn largeBlob.write extension behaviour on top of webauthn_make_credential / webauthn_get_assertion.
  • Once both halves land, end-to-end roundtrip tests will be added in the integration-test surface (e.g. a future libwebauthn-tests crate).

Tests

The PR adds 16 unit tests, including a MockChannel-driven end-to-end test of webauthn_get_assertion with largeBlob.read = true: the test builds a real serialized largeBlobArray (AES-256-GCM ciphertext + nonce + origSize + SHA-256 trailer), responds to the GetInfo / GetAssertion / authenticatorLargeBlobs(get) sequence, calls webauthn_get_assertion, and asserts that the returned unsigned_extensions_output.large_blob.blob matches the original plaintext. That covers the entire read pipeline: CTAP exchange, array parsing, AES-256-GCM decrypt, deflate decompress, trait surface, and WebAuthn JSON output.

Other tests cover: MemoryLargeBlobStorage round-trip / missing-credential / multi-credential; AES-256-GCM round-trip and wrong-key skip; multi-entry array selection; corrupted-trailer and truncated-array rejection; empty-array handling; Ctap2LargeBlobsRequest canonical CBOR encoding; an AuthenticatorLargeBlobStorage::read integration test against a hand-crafted ciphertext via MockChannel; and AuthenticatorLargeBlobStorage::write returning Unsupported.

Dependencies

  • aes-gcm = "0.10" (AES-256-GCM AEAD)
  • flate2 = "1.0" (RFC 1951 raw deflate)

Spec references

@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/webauthn-largeblob-key-disclosure branch from 4f484fb to 63fd2d2 Compare May 10, 2026 20:26
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the feat/webauthn-largeblob-storage branch from 1129c2d to 5a50879 Compare May 10, 2026 20:43
AlfioEmanueleFresta added a commit that referenced this pull request May 12, 2026
Per review on #198: keep the per-credential largeBlobKey only on the
CTAP-level Ctap2GetAssertionResponse. Surfacing it on the public
Assertion struct gives callers a foot-gun to forward straight to the
RP, which is exactly the disclosure this PR is meant to prevent. The
follow-up authenticatorLargeBlobs PR (#206) can read the key directly
off the CTAP response.
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/webauthn-largeblob-key-disclosure branch from 63fd2d2 to 34571aa Compare May 12, 2026 17:46
AlfioEmanueleFresta added a commit that referenced this pull request May 12, 2026
Per review on #198: keep the per-credential largeBlobKey only on the
CTAP-level Ctap2GetAssertionResponse. Surfacing it on the public
Assertion struct gives callers a foot-gun to forward straight to the
RP, which is exactly the disclosure this PR is meant to prevent. The
follow-up authenticatorLargeBlobs PR (#206) can read the key directly
off the CTAP response.
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/webauthn-largeblob-key-disclosure branch from 34571aa to 5388273 Compare May 12, 2026 17:47
AlfioEmanueleFresta added a commit that referenced this pull request May 12, 2026
Per [WebAuthn L3 sec. 10.1.5 (largeBlob
extension)](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension),
the relying party expects the `blob` output to be the decrypted
plaintext blob payload, fetched by the platform via the CTAP
[`authenticatorLargeBlobs`](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorLargeBlobs)
command using the per-credential `largeBlobKey` as an AES-256-GCM key.
The library was instead writing the raw `largeBlobKey` into `blob` and
never calling `authenticatorLargeBlobs`. That means an RP receives the
AES key itself (not the blob), and if the RP can also read the device's
`largeBlobArray` (publicly readable region of the authenticator over
CTAP), it can decrypt and forge entries.

Until `authenticatorLargeBlobs` is wired up (follow-up PR #206), the
safe behaviour is to set `large_blob.blob = None`. The CTAP-level model
keeps the field so the follow-up can use it.

## Changes

- Stop routing `largeBlobKey` into the WebAuthn `large_blob.blob`
output.
- The CTAP-level `Ctap2GetAssertionResponse.large_blob_key` remains so
the next PR can use it.
- Regression test asserts the WebAuthn response no longer contains the
key.

## References

- [WebAuthn L3 sec. 10.1.5: Large blob storage extension
(`largeBlob`)](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension)
- [CTAP 2.1 sec. 6.10: `authenticatorLargeBlobs`
(0x0C)](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorLargeBlobs)
Base automatically changed from fix/webauthn-largeblob-key-disclosure to master May 12, 2026 17:51
Implements the wire-level model and protocol method for CTAP 2.1
`authenticatorLargeBlobs` (command code 0x0C, spec §6.10). This is the
device-side primitive the platform uses to fetch and update the
authenticator's serialized largeBlobArray.

Includes only the `get` request shape so far; `set` is reserved for a
follow-up that will also handle the pinUvAuthParam binding required for
writes.

Refs: CTAP 2.2 §6.10.
…ckends

Adds the public `LargeBlobStorage` async trait alongside two bundled
implementations:

- `MemoryLargeBlobStorage`: a HashMap-backed store, primarily for tests.
- `AuthenticatorLargeBlobStorage<'_, C>`: drives the CTAP 2.1
  `authenticatorLargeBlobs(get)` command, parses the serialized
  largeBlobArray, locates the entry matching the supplied per-credential
  `largeBlobKey` (AES-256-GCM authenticated decryption), and decompresses
  the deflated plaintext.

Only the read path is implemented in this PR. `LargeBlobStorage::write`
returns `Unsupported` in both bundled backends; the chunked write path
with `pinUvAuthParam` binding is reserved for a follow-up.

Includes 14 unit tests covering: in-memory round-trip, AEAD round-trip,
wrong-key rejection, multi-entry array selection, corrupted/truncated
array rejection, empty array handling, and a MockChannel-backed end-to-end
test of the authenticator read flow.

Refs: WebAuthn L3 §10.5, CTAP 2.2 §6.10 / §6.10.4 / §11.4.
When the WebAuthn `largeBlob: { read: true }` extension is requested and
the authenticator returns a per-credential `largeBlobKey`, libwebauthn
now runs `authenticatorLargeBlobs(get)` to fetch the on-device serialized
array, decrypts the matching entry, and exposes the plaintext via the
WebAuthn response's `unsigned_extensions_output.large_blob.blob` field.

The read flow uses a per-assertion `AuthenticatorLargeBlobStorage` handle
(introduced in the previous commit), so each credential is read against
its own `largeBlobKey`. Failures are non-fatal: per WebAuthn L3 §10.5 the
`blob` output is optional on success.

Combined with the earlier fix that removed the key-disclosure bug, this
completes the read half of the WebAuthn `largeBlob` extension.

Refs: WebAuthn L3 §10.5, CTAP 2.2 §6.10.
- GetAssertionRequest.cross_origin renamed to top_origin (#188)
- Assertion no longer carries large_blob_key (#198); thread it
  through from Ctap2GetAssertionResponse instead
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the feat/webauthn-largeblob-storage branch from 5a50879 to 33be166 Compare May 12, 2026 18:41
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as ready for review May 12, 2026 18:41
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as draft May 12, 2026 18:45
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