first commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# largeBlob extension support
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §10.1.3 largeBlob extension passthrough plus result parsing. The largeBlob extension lets the authenticator store a small per-credential byte string (typically credentials or session-resumption tokens) inside the authenticator's secure element. Registration asks the authenticator to allocate the slot; subsequent authentications read or write the blob.
|
||||
|
||||
## Features
|
||||
|
||||
- `LargeBlobRegisterInput` (`{ support: "required" | "preferred" }`) and discriminated `LargeBlobAuthenticateInput` (`{ read: true }` xor `{ write: Uint8Array }`) — W3C-mandated mutual exclusion.
|
||||
- `readLargeBlobRegisterResult` extracts `{ supported }` from the registration ceremony's `ClientExtensionResults`.
|
||||
- `readLargeBlobReadResult` extracts `{ blob? }` from the assertion ceremony's results — `blob` is omitted (not zeroed) when the authenticator did not return one.
|
||||
- `readLargeBlobWriteResult` extracts `{ written }` — boolean discriminator between success and silent drop.
|
||||
- `buildLargeBlobRegisterExtension` / `buildLargeBlobAuthenticateExtension` produce the wire-shape passed to `navigator.credentials.{create,get}`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildLargeBlobAuthenticateExtension,
|
||||
readLargeBlobReadResult,
|
||||
} from "@suaveplan/webauthn/extensions/large-blob";
|
||||
|
||||
const ext = buildLargeBlobAuthenticateExtension({ read: true });
|
||||
const cred = await navigator.credentials.get({
|
||||
publicKey: { challenge, allowCredentials, extensions: ext }
|
||||
});
|
||||
const result = readLargeBlobReadResult(
|
||||
(cred as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
console.log(result?.blob?.byteLength ?? 0);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildLargeBlobAuthenticateExtension,
|
||||
buildLargeBlobRegisterExtension,
|
||||
readLargeBlobRegisterResult,
|
||||
readLargeBlobWriteResult,
|
||||
} from "@suaveplan/webauthn/extensions/large-blob";
|
||||
|
||||
const regExt = buildLargeBlobRegisterExtension({ support: "required" });
|
||||
const reg = await navigator.credentials.create({
|
||||
publicKey: { ...opts, extensions: regExt }
|
||||
});
|
||||
const regResult = readLargeBlobRegisterResult(
|
||||
(reg as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!regResult?.supported) {
|
||||
throw new Error("authenticator does not support largeBlob");
|
||||
}
|
||||
|
||||
const writeExt = buildLargeBlobAuthenticateExtension({ write: payload });
|
||||
const authResult = await navigator.credentials.get({
|
||||
publicKey: { ...authOpts, extensions: writeExt }
|
||||
});
|
||||
const w = readLargeBlobWriteResult(
|
||||
(authResult as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!w?.written) {
|
||||
throw new Error("write dropped");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LargeBlobRegisterInput`, `LargeBlobAuthenticateInput` — input shapes.
|
||||
- `LargeBlobRegisterResult`, `LargeBlobReadResult`, `LargeBlobWriteResult` — result shapes.
|
||||
- `readLargeBlobRegisterResult`, `readLargeBlobReadResult`, `readLargeBlobWriteResult` — narrowing readers.
|
||||
- `buildLargeBlobRegisterExtension`, `buildLargeBlobAuthenticateExtension` — wire-shape builders.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `support: "required"` mode causes the registration to fail (with `NotSupportedError`) when the authenticator cannot allocate a largeBlob slot; `support: "preferred"` allocates if possible and falls back silently otherwise. The result's `supported` flag distinguishes the two outcomes.
|
||||
- Read and write are mutually exclusive per W3C §10.1.3. The discriminated input union enforces this at the type level so callers cannot accidentally pass both.
|
||||
- `readLargeBlobReadResult` returns `{}` (empty object, not `undefined`) when the extension was acknowledged but no `blob` was returned. This lets callers distinguish "authenticator does not implement the extension" (`undefined` return) from "extension implemented, no blob stored" (`{ blob: undefined }`).
|
||||
62
docs/identity/auth/webauthn/src/extensions/prf/prf.md
Normal file
62
docs/identity/auth/webauthn/src/extensions/prf/prf.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# PRF extension support
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the W3C WebAuthn L3 §10.1.4 PRF (Pseudo-Random Function) extension passthrough plus result parsing. PRF is the cornerstone of passkey-as-master-key-unlock flows: the authenticator's secure element materialises a ~32-byte symmetric secret deterministic in the (credential id, caller-supplied salt) pair. Consumers (notably `@suaveplan/vault`) use the secret to wrap an Account Unlock Key so a vault unlocks without the user typing a master password.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed input shapes for both ceremonies: `PrfRegisterInput` (`{ eval: { first, second? } }`) and `PrfAuthenticateInput` (`{ eval | evalByCredential }`).
|
||||
- `readPrfResult(extensions)` extracts the `prf` field from the raw `ClientExtensionResults` object, narrowing types so callers consume `Uint8Array`s rather than `unknown`.
|
||||
- `buildPrfExtension(input)` produces the wire-shape forwarded to `navigator.credentials.create/get` so callers do not duplicate the structural construction.
|
||||
- Three-state result: `enabled === false` (not supported), `enabled === true` with no `results` (supported but caller did not request an eval), `enabled === true` with `results.first: Uint8Array` (the materialised secret).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readPrfResult } from "@suaveplan/webauthn/extensions/prf";
|
||||
|
||||
const prf = readPrfResult(authenticationResult.extensions);
|
||||
if (prf?.enabled && prf.results?.first) {
|
||||
const wrapKey = prf.results.first;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildPrfExtension,
|
||||
readPrfResult,
|
||||
} from "@suaveplan/webauthn/extensions/prf";
|
||||
|
||||
const ext = buildPrfExtension({
|
||||
eval: { first: new Uint8Array(32) }
|
||||
});
|
||||
const cred = await navigator.credentials.get({
|
||||
publicKey: { challenge, allowCredentials, extensions: ext },
|
||||
});
|
||||
const prf = readPrfResult(
|
||||
(cred as PublicKeyCredential).getClientExtensionResults()
|
||||
);
|
||||
if (!prf?.enabled) {
|
||||
throw new Error("authenticator does not support PRF");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PrfRegisterInput` — `{ eval?: { first: Uint8Array, second?: Uint8Array } }`.
|
||||
- `PrfAuthenticateInput` — `{ eval?, evalByCredential? }`.
|
||||
- `PrfRegisterResult` / `PrfAuthenticateResult` — `{ enabled: boolean, results?: { first: Uint8Array, second?: Uint8Array } }`.
|
||||
- `readPrfResult(extensions): PrfRegisterResult | undefined` — narrow + extract.
|
||||
- `buildPrfExtension(input): { prf?: ... }` — wire-shape builder.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The package does NOT request or surface a `prf` second-input by default. Consumers that need two-input PRF (key rotation flows) opt in by setting `eval.second`.
|
||||
- The `enabled` field is set to `true` by some Chromium platform authenticators even when the caller did not request an eval; consumers SHOULD treat `enabled === true` without `results` as "PRF is available, request it next time" rather than "secret is materialised".
|
||||
- `readPrfResult` is intentionally tolerant of missing fields — it returns `undefined` when no PRF extension is present at all, distinguishing that from `{ enabled: false }` (PRF requested but not supported by the authenticator).
|
||||
Reference in New Issue
Block a user