From a8c641e9ad35cc48f36cc1451234c0f15451e602 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 3 Oct 2025 08:33:54 -0700 Subject: [PATCH 01/19] Add basic passkey implementation tutorial --- .../integrations/passkeys/index.md | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/blockchain-development-tutorials/integrations/passkeys/index.md diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md new file mode 100644 index 0000000000..301e6a1768 --- /dev/null +++ b/docs/blockchain-development-tutorials/integrations/passkeys/index.md @@ -0,0 +1,422 @@ +--- +title: Passkeys (WebAuthn) on Flow — Registration and Signing +description: Implement passkeys on Flow using WebAuthn, covering key extraction, challenges, signature formatting for Flow, and signature extensions. +sidebar_position: 3 +keywords: + - passkeys + - WebAuthn + - FCL + - account proof + - signature extension + - ECDSA_P256 + - SHA2_256 +--- + +# Passkeys (WebAuthn) on Flow — Registration and Signing + +This guide shows how to implement passkeys with WebAuthn for Flow accounts, focusing on four practical areas that often trip teams up: + +1. Extracting the public key and attaching it to a new Flow account +2. Generating the challenge to be signed +3. Formatting a passkey signature for Flow and attaching it to a transaction +4. Generating and attaching the signature extension data + +This tutorial is implementation‑focused and describes a wallet‑centric integration per the FLIP (wallet is the WebAuthn Relying Party). It accompanies an internal Proof of Concept (PoC) built in `fcl-js` under `packages/passkey-wallet` for reference. + +> PoC source: `/Users/jribbink/repos/fcl-js/packages/passkey-wallet` + +## Objectives + +After completing this guide, you'll be able to: + +- Register a WebAuthn credential and derive a Flow‑compatible public key +- Set wallet‑mode challenges per FLIP (constant for registration, payload hash for signing) +- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format +- Attach WebAuthn signature extension data per the FLIP and submit a Flow transaction + +## Prerequisites + +- Working knowledge of modern frontend (React/Next.js) and basic backend +- Familiarity with WebAuthn/Passkeys concepts and platform constraints +- FCL installed and configured for your app +- A plan for secure backend entropy (32‑byte minimum) and nonce persistence + +See also: + +- Transactions and signatures on Flow: `../../build/cadence/basics/transactions.md` +- Account keys, signature and hash algorithms: `../../build/cadence/basics/accounts.md` + +External references: + +- WebAuthn credential support FLIP: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) +- Web Authentication API (MDN): [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) + +## 1) Create a user passkey wallet + +When a user registers a passkey via `navigator.credentials.create({ publicKey })`, the authenticator returns an attestation containing the new credential’s public key. For Flow, you’ll register that public key on an account as an `ECDSA_P256` key paired with `SHA2_256` hashing. + +High‑level steps: + +1. On the server, generate `PublicKeyCredentialCreationOptions` and send to the client. +2. On the client, call WebAuthn `create()` and return the credential to the server. +3. Verify attestation if necessary and extract the COSE public key (P‑256). Convert it to the raw uncompressed X9.62 point format if needed and to hex bytes expected by Flow. +4. Submit a transaction to add the key to the Flow account with weight and algorithms: + - Signature algorithm: `ECDSA_P256` + - Hash algorithm: `SHA2_256` + +Key algorithm references: `../../build/cadence/basics/accounts.md` (Signature and Hash Algorithms). + +> Tip: Libraries like SimpleWebAuthn can parse the COSE key and produce the raw public key bytes required for onchain registration. Ensure you normalize into the exact raw byte format Flow expects before writing to the account key. + +Minimum example — wallet‑mode registration (challenge can be constant per FLIP): + +```tsx +// In a wallet (RP = wallet origin). The challenge satisfies API & correlates request/response. +// Use a stable, opaque user.id per wallet user (do not randomize per request). + +const rp = { name: "Passkey Wallet", id: window.location.hostname } as const +const user = { + id: getStableUserIdBytes(), // Uint8Array (16–64 bytes) stable per user + name: "flow-user", + displayName: "Flow User", +} as const + +const creationOptions: PublicKeyCredentialCreationOptions = { + challenge: new TextEncoder().encode("flow-wallet-register"), // constant is acceptable in wallet-mode; wallet providers may choose and use a constant value as needed for correlation + rp, + user, + pubKeyCredParams: [ + { type: "public-key", alg: -7 }, // ES256 (P-256 + SHA-256) + // Optionally ES256K if you support secp256k1 Flow keys: + // { type: "public-key", alg: -47 }, + ], + authenticatorSelection: { userVerification: "preferred" }, + timeout: 60_000, + attestation: "none", +} + +const credential = await navigator.credentials.create({ publicKey: creationOptions }) + +// Send to wallet-core (or local) to extract COSE P-256 public key (verify attestation if necessary) +// Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 +``` + +Client-side example — extract COSE public key (no verification) and derive SEC1 uncompressed hex: + +```tsx +// Uses a small CBOR decoder (e.g., 'cbor' or 'cbor-x') to parse attestationObject +import * as CBOR from 'cbor' + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') +} + +function extractCosePublicKeyFromAttestation(attObj: Uint8Array): Uint8Array { + // attestationObject is a CBOR map with 'authData' + const decoded: any = CBOR.decode(attObj) + const authData = new Uint8Array(decoded.authData) + + // Parse authData (WebAuthn spec): + // rpIdHash(32) + flags(1) + signCount(4) = 37 bytes header + let offset = 37 + // aaguid (16) + offset += 16 + // credentialId length (2 bytes, big-endian) + const credIdLen = (authData[offset] << 8) | authData[offset + 1] + offset += 2 + // credentialId (credIdLen bytes) + offset += credIdLen + // The next CBOR structure is the credentialPublicKey (COSE key) + return authData.slice(offset) +} + +function coseEcP256ToSec1UncompressedHex(coseKey: Uint8Array): string { + // COSE EC2 key is a CBOR map; for P-256, x = -2, y = -3 + const m: Map = CBOR.decode(coseKey) + const x = new Uint8Array(m.get(-2)) + const y = new Uint8Array(m.get(-3)) + if (x.length !== 32 || y.length !== 32) throw new Error('Invalid P-256 coordinate lengths') + const sec1 = new Uint8Array(65) + sec1[0] = 0x04 + sec1.set(x, 1) + sec1.set(y, 33) + return '0x' + toHex(sec1) +} + +// Usage +const cred = (await navigator.credentials.create({ publicKey: creationOptions })) as PublicKeyCredential +const att = cred.response as AuthenticatorAttestationResponse +const attObj = new Uint8Array(att.attestationObject as ArrayBuffer) +const cosePubKey = extractCosePublicKeyFromAttestation(attObj) +const publicKeySec1Hex = coseEcP256ToSec1UncompressedHex(cosePubKey) +``` + +## 2) Sign a transaction with passkey wallet (WebAuthn) + +### Generate the challenge + +- Assertion (transaction signing): Wallet sets `challenge` to the SHA2‑256 of the signable transaction message (payload or envelope per signer role). No server‑sent challenge is used. Flow includes a domain‑separation tag in the signable bytes. + +Minimal example — derive signable message and hash (per FLIP): + +```tsx +// Imports for helpers used to build the signable message +import { encodeMessageFromSignable, encodeTransactionPayload } from '@onflow/fcl' +// Hash/encoding utilities (example libs) +import { sha256 } from '@noble/hashes/sha256' +import { hexToBytes } from '@noble/hashes/utils' + +// Inputs: +// - signable: object containing the voucher/payload bytes (e.g., from a ready payload) +// - address: the signing account address (hex string) + +declare const signable: any +declare const address: string + +// 1) Encode the signable message for this signer (payload vs envelope) +const msgHex = encodeMessageFromSignable(signable, address) +const payloadMsgHex = encodeTransactionPayload(signable.voucher) +const role = msgHex === payloadMsgHex ? "payload" : "envelope" + +// 2) Compute SHA2-256(msgHex) -> 32-byte challenge (Flow keys commonly use SHA2_256) +const signableHash: Uint8Array = sha256(hexToBytes(msgHex)) + +// 3) Call navigator.credentials.get with challenge = signableHash +// (see next subsection for a full getAssertion example) +``` + +Note: `encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑specific helpers. If you are not using FCL, construct the Flow signable transaction message yourself (payload for proposer/authorizer, envelope for payer), then compute `SHA2‑256(messageBytes)` for the challenge. The payload encoding shown here applies regardless of wallet implementation; the helper calls are simply conveniences from FCL. + +### Sign with the user's passkey + +Minimal example — wallet assertion: + +```tsx +// signableHash is SHA2-256(signable message: payload or envelope) +declare const signableHash: Uint8Array + +const requestOptions: PublicKeyCredentialRequestOptions = { + challenge: signableHash, + rpId: window.location.hostname, + userVerification: "preferred", + timeout: 60_000, +} + +const assertion = (await navigator.credentials.get({ + publicKey: requestOptions, +})) as PublicKeyCredential + +const { authenticatorData, clientDataJSON, signature } = + assertion.response as AuthenticatorAssertionResponse +``` + +### Format properly and submit to network + +- Convert the DER `signature` to Flow raw `r||s` (64 bytes) and attach with `addr` and `keyId`. +- Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. +- See details below in sections 3 and 4. + +Minimal example — convert and submit: + +```tsx +import { encode as rlpEncode } from 'rlp' +import { AppUtils } from '@onflow/fcl' +import { bytesToHex } from '@noble/hashes/utils' + +// Inputs from previous steps +declare const address: string // 0x-prefixed Flow address +declare const keyId: number // Account key index used for signing +declare const signature: Uint8Array // DER signature from WebAuthn assertion +declare const clientDataJSON: Uint8Array +declare const authenticatorData: Uint8Array + +// 1) DER -> raw r||s (64 bytes) +const rawSig = derToRawRS(signature) + +// 2) Build extension_data per FLIP: 0x01 || RLP([authenticatorData, clientDataJSON]) +const rlpPayload = rlpEncode([authenticatorData, clientDataJSON]) as Uint8Array | Buffer +const rlpBytes = rlpPayload instanceof Uint8Array ? rlpPayload : new Uint8Array(rlpPayload) +const extension_data = new Uint8Array(1 + rlpBytes.length) +extension_data[0] = 0x01 +extension_data.set(rlpBytes, 1) + +// 3) Compose Flow signature object +const flowSignature = { + addr: address, // e.g., '0x1cf0e2f2f715450' + keyId, // integer key index + signature: '0x' + bytesToHex(rawSig), + signatureExtension: extension_data, +} + +// 4) Submit transaction (placeholder — depends on wallet implementation) +// await fcl.send([... build tx ... with flowSignature and extension_data ...]) +``` + +Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). + +Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits; a backend is not required by the FLIP. + +On the backend, persist the nonce briefly for verification and expiry. See `../../build/tools/clients/fcl-js/proving-authentication.mdx` for the end‑to‑end flow and server verification using `AppUtils.verifyAccountProof`. + +### Allowed algorithms for WebAuthn credentials + +Restrict `pubKeyCredParams` to algorithms supported by Flow accounts: + +```tsx +// ES256 (P-256 + SHA-256) is recommended and maps to ECDSA_P256/SHA2_256 on Flow +pubKeyCredParams: [ + { type: "public-key", alg: -7 }, // ES256 + // Optionally, if you support secp256k1 keys as Flow account keys: + // { type: "public-key", alg: -47 }, // ES256K (secp256k1) +] +``` + +Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. + +## 3) Format the passkey signature for Flow and attach it to the transaction + +WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). + +High‑level steps when using a passkey to sign a Flow transaction: + +1. Build the Flow transaction payload that needs signing (RLP‑encoded payload per Flow signing rules). +2. Produce a WebAuthn `navigator.credentials.get({ publicKey })` assertion whose effective challenge maps to the Flow payload per the signature extension specification. +3. Convert the DER signature from the authenticator into raw `r||s` (pad to 32 bytes per component). +4. Attach the signature to the appropriate signature set (payload or envelope) with `addr`, `keyId`, and the raw signature bytes. + +> The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. + +Useful references: + +- Transactions/signatures: `../../build/cadence/basics/transactions.md` +- User signatures via FCL: `../../build/tools/clients/fcl-js/user-signatures.md` +- FLIP spec: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) + +Minimum example — wallet‑mode assertion using Flow payload hash as challenge: + +```tsx +// payloadHash should be the 32-byte hash of the Flow RLP-encoded payload +// (computed by your signing logic). It must be the challenge in wallet-mode. +declare const payloadHash: Uint8Array + +const requestOptions: PublicKeyCredentialRequestOptions = { + challenge: payloadHash, + rpId: window.location.hostname, + userVerification: "preferred", + timeout: 60_000, +} + +const assertion = (await navigator.credentials.get({ + publicKey: requestOptions, +})) as PublicKeyCredential + +const { authenticatorData, clientDataJSON, signature } = (assertion.response as AuthenticatorAssertionResponse) + +const extensionData = { + clientDataJSON: toBase64Url(new Uint8Array(clientDataJSON)), + authenticatorData: toBase64Url(new Uint8Array(authenticatorData)), +} + +// Convert DER signature to Flow raw r||s +const rawSig = derToRawRS(new Uint8Array(signature)) // Uint8Array(64) + +// Attach to Flow transaction (addr, keyId, signature = rawSig) +// and include extensionData as specified by the FLIP’s signature extension +``` + +Helpers: + +```tsx +function toBase64Url(bytes: Uint8Array): string { + const base64 = btoa(String.fromCharCode(...bytes)) + return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") +} + +// Minimal DER ECDSA (r,s) -> raw 64-byte r||s +function derToRawRS(der: Uint8Array): Uint8Array { + let offset = 0 + if (der[offset++] !== 0x30) throw new Error("Invalid DER sequence") + const seqLen = der[offset++] // assumes short form + if (seqLen + 2 !== der.length) throw new Error("Invalid DER length") + + if (der[offset++] !== 0x02) throw new Error("Missing r INTEGER") + const rLen = der[offset++] + let r = der.slice(offset, offset + rLen) + offset += rLen + if (der[offset++] !== 0x02) throw new Error("Missing s INTEGER") + const sLen = der[offset++] + let s = der.slice(offset, offset + sLen) + + // Strip leading zeros and left-pad to 32 bytes + r = stripLeadingZeros(r) + s = stripLeadingZeros(s) + const r32 = leftPad32(r) + const s32 = leftPad32(s) + const raw = new Uint8Array(64) + raw.set(r32, 0) + raw.set(s32, 32) + return raw +} + +function stripLeadingZeros(bytes: Uint8Array): Uint8Array { + let i = 0 + while (i < bytes.length - 1 && bytes[i] === 0x00) i++ + return bytes.slice(i) +} + +function leftPad32(bytes: Uint8Array): Uint8Array { + if (bytes.length > 32) throw new Error("Component too long") + const out = new Uint8Array(32) + out.set(bytes, 32 - bytes.length) + return out +} +``` + +## 4) Generate and attach the signature extension data + +Flow’s transaction signature extensions allow additional data to be provided alongside a signature so verifiers can reconstruct and validate the authenticator’s signed message. For WebAuthn, this typically includes: + +- `clientDataJSON` (base64url) +- `authenticatorData` (base64url) +- The authenticator’s signature (converted to raw `r||s` for Flow) +- Any FLIP‑required metadata (e.g., `type`, `rpIdHash`, `origin` semantics) + +High‑level steps: + +1. After `navigator.credentials.get`, collect: + - `assertion.response.clientDataJSON` + - `assertion.response.authenticatorData` + - `assertion.response.signature` +2. Encode `clientDataJSON` and `authenticatorData` as base64url. +3. Convert the DER signature to Flow raw `r||s`. +4. Attach the extension structure specified in the FLIP to the transaction signature so nodes/verifiers can validate according to WebAuthn rules. + +> The exact extension schema and how it is serialized into the transaction is defined in the FLIP. Implementations should follow the FLIP verbatim to remain compatible with network verification. + +Reference: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) + +## Notes from the PoC + +- The PoC in `fcl-js/packages/passkey-wallet` demonstrates end‑to‑end flows for passkey creation and assertion, including: + - Extracting and normalizing the P‑256 public key for Flow + - Generating secure nonces and verifying account‑proof + - Converting DER signatures to raw `r||s` + - Packaging WebAuthn fields as signature extension data + +> Align your implementation with the FLIP to ensure your extension payloads and verification logic match network expectations. + +## Security and UX considerations + +- Use `ECDSA_P256` with `SHA2_256` for Flow account keys derived from WebAuthn P‑256. +- Enforce nonce expiry, single‑use semantics, and strong server‑side randomness. +- Clearly communicate platform prompts and recovery paths; passkeys UX can differ across OS/browsers. +- Consider fallbacks for non‑WebAuthn environments. + +## Where to go next + +- Implement account‑proof using `AppUtils.verifyAccountProof`: `../../build/tools/clients/fcl-js/proving-authentication.mdx` +- Review signing flows and signature roles: `../../build/cadence/basics/transactions.md` +- Review account key registration details: `../../build/cadence/basics/accounts.md` +- Track the FLIP for any updates: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) + + From c5856bcafc7e12c000b9f7c41260083d64ffc6eb Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 3 Oct 2025 08:45:27 -0700 Subject: [PATCH 02/19] tidy --- .../integrations/passkeys/index.md | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md index 301e6a1768..2f0b741665 100644 --- a/docs/blockchain-development-tutorials/integrations/passkeys/index.md +++ b/docs/blockchain-development-tutorials/integrations/passkeys/index.md @@ -210,7 +210,9 @@ const { authenticatorData, clientDataJSON, signature } = assertion.response as AuthenticatorAssertionResponse ``` -### Format properly and submit to network +### Format the signature for Flow + +WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). - Convert the DER `signature` to Flow raw `r||s` (64 bytes) and attach with `addr` and `keyId`. - Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. @@ -247,16 +249,11 @@ const flowSignature = { signature: '0x' + bytesToHex(rawSig), signatureExtension: extension_data, } - -// 4) Submit transaction (placeholder — depends on wallet implementation) -// await fcl.send([... build tx ... with flowSignature and extension_data ...]) ``` -Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). +Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than traditional WebAuthn server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). -Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits; a backend is not required by the FLIP. - -On the backend, persist the nonce briefly for verification and expiry. See `../../build/tools/clients/fcl-js/proving-authentication.mdx` for the end‑to‑end flow and server verification using `AppUtils.verifyAccountProof`. +Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits, however a backend is not explicitly required. ### Allowed algorithms for WebAuthn credentials @@ -273,16 +270,11 @@ pubKeyCredParams: [ Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. -## 3) Format the passkey signature for Flow and attach it to the transaction +## 3) WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). -High‑level steps when using a passkey to sign a Flow transaction: - -1. Build the Flow transaction payload that needs signing (RLP‑encoded payload per Flow signing rules). -2. Produce a WebAuthn `navigator.credentials.get({ publicKey })` assertion whose effective challenge maps to the Flow payload per the signature extension specification. -3. Convert the DER signature from the authenticator into raw `r||s` (pad to 32 bytes per component). -4. Attach the signature to the appropriate signature set (payload or envelope) with `addr`, `keyId`, and the raw signature bytes. +This section focuses on converting the WebAuthn signature to Flow raw `r||s` and how/where to attach it; the end‑to‑end flow (building the signable message, generating the challenge, and calling `navigator.credentials.get`) is shown in section 2. > The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. From 3c2e3592dbe74d9e71aab851127352e952120bcf Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 3 Oct 2025 08:54:21 -0700 Subject: [PATCH 03/19] cleanup --- .../integrations/passkeys/index.md | 83 ++++++------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md index 2f0b741665..1caa460e00 100644 --- a/docs/blockchain-development-tutorials/integrations/passkeys/index.md +++ b/docs/blockchain-development-tutorials/integrations/passkeys/index.md @@ -6,26 +6,25 @@ keywords: - passkeys - WebAuthn - FCL - - account proof - signature extension - ECDSA_P256 - SHA2_256 --- -# Passkeys (WebAuthn) on Flow — Registration and Signing +# Passkeys (WebAuthn) on Flow — Wallet Implementation Guide -This guide shows how to implement passkeys with WebAuthn for Flow accounts, focusing on four practical areas that often trip teams up: +This is a wallet‑centric guide (per the FLIP) that covers end‑to‑end WebAuthn integration for Flow: -1. Extracting the public key and attaching it to a new Flow account -2. Generating the challenge to be signed -3. Formatting a passkey signature for Flow and attaching it to a transaction -4. Generating and attaching the signature extension data +1. Create a user passkey wallet +2. Sign a transaction with the user’s passkey +3. Convert and attach the signature +4. Build the signature extension payload -This tutorial is implementation‑focused and describes a wallet‑centric integration per the FLIP (wallet is the WebAuthn Relying Party). It accompanies an internal Proof of Concept (PoC) built in `fcl-js` under `packages/passkey-wallet` for reference. +It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. > PoC source: `/Users/jribbink/repos/fcl-js/packages/passkey-wallet` -## Objectives +## What you’ll learn After completing this guide, you'll be able to: @@ -41,12 +40,12 @@ After completing this guide, you'll be able to: - FCL installed and configured for your app - A plan for secure backend entropy (32‑byte minimum) and nonce persistence -See also: +See also - Transactions and signatures on Flow: `../../build/cadence/basics/transactions.md` - Account keys, signature and hash algorithms: `../../build/cadence/basics/accounts.md` -External references: +External references - WebAuthn credential support FLIP: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) - Web Authentication API (MDN): [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) @@ -70,6 +69,8 @@ Key algorithm references: `../../build/cadence/basics/accounts.md` (Signature an Minimum example — wallet‑mode registration (challenge can be constant per FLIP): +This builds `PublicKeyCredentialCreationOptions` for a wallet RP with a constant registration challenge and ES256 (P‑256) so the resulting public key can be registered on a Flow account. + ```tsx // In a wallet (RP = wallet origin). The challenge satisfies API & correlates request/response. // Use a stable, opaque user.id per wallet user (do not randomize per request). @@ -103,6 +104,8 @@ const credential = await navigator.credentials.create({ publicKey: creationOptio Client-side example — extract COSE public key (no verification) and derive SEC1 uncompressed hex: +This parses the `attestationObject` to locate the COSE EC2 `credentialPublicKey`, reads the x/y coordinates, and returns SEC1 uncompressed bytes (0x04 || X || Y) suitable for Flow key registration. Attestation verification is intentionally omitted here. + ```tsx // Uses a small CBOR decoder (e.g., 'cbor' or 'cbor-x') to parse attestationObject import * as CBOR from 'cbor' @@ -159,6 +162,8 @@ const publicKeySec1Hex = coseEcP256ToSec1UncompressedHex(cosePubKey) Minimal example — derive signable message and hash (per FLIP): +Compute the signer‑specific signable message and hash it with SHA2‑256 to produce the WebAuthn `challenge` (no server‑generated nonce is used in wallet mode). + ```tsx // Imports for helpers used to build the signable message import { encodeMessageFromSignable, encodeTransactionPayload } from '@onflow/fcl' @@ -191,6 +196,8 @@ Note: `encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑speci Minimal example — wallet assertion: +Request an assertion using the transaction hash as `challenge`. `rpId` must match the wallet domain; `allowCredentials` may be omitted for discoverable credentials. + ```tsx // signableHash is SHA2-256(signable message: payload or envelope) declare const signableHash: Uint8Array @@ -210,7 +217,7 @@ const { authenticatorData, clientDataJSON, signature } = assertion.response as AuthenticatorAssertionResponse ``` -### Format the signature for Flow +### 3) Convert and attach the signature WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). @@ -218,7 +225,9 @@ WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically - Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. - See details below in sections 3 and 4. -Minimal example — convert and submit: +Minimal example — convert and attach for submission: + +Convert the DER signature to Flow raw r||s and build `signatureExtension = 0x01 || RLP([authenticatorData, clientDataJSON])` per the FLIP, then compose the Flow signature object for inclusion in your transaction. ```tsx import { encode as rlpEncode } from 'rlp' @@ -270,11 +279,7 @@ pubKeyCredParams: [ Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. -## 3) - -WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). - -This section focuses on converting the WebAuthn signature to Flow raw `r||s` and how/where to attach it; the end‑to‑end flow (building the signable message, generating the challenge, and calling `navigator.credentials.get`) is shown in section 2. +## Appendix: Helpers > The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. @@ -284,46 +289,9 @@ Useful references: - User signatures via FCL: `../../build/tools/clients/fcl-js/user-signatures.md` - FLIP spec: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) -Minimum example — wallet‑mode assertion using Flow payload hash as challenge: +Helpers used above: ```tsx -// payloadHash should be the 32-byte hash of the Flow RLP-encoded payload -// (computed by your signing logic). It must be the challenge in wallet-mode. -declare const payloadHash: Uint8Array - -const requestOptions: PublicKeyCredentialRequestOptions = { - challenge: payloadHash, - rpId: window.location.hostname, - userVerification: "preferred", - timeout: 60_000, -} - -const assertion = (await navigator.credentials.get({ - publicKey: requestOptions, -})) as PublicKeyCredential - -const { authenticatorData, clientDataJSON, signature } = (assertion.response as AuthenticatorAssertionResponse) - -const extensionData = { - clientDataJSON: toBase64Url(new Uint8Array(clientDataJSON)), - authenticatorData: toBase64Url(new Uint8Array(authenticatorData)), -} - -// Convert DER signature to Flow raw r||s -const rawSig = derToRawRS(new Uint8Array(signature)) // Uint8Array(64) - -// Attach to Flow transaction (addr, keyId, signature = rawSig) -// and include extensionData as specified by the FLIP’s signature extension -``` - -Helpers: - -```tsx -function toBase64Url(bytes: Uint8Array): string { - const base64 = btoa(String.fromCharCode(...bytes)) - return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") -} - // Minimal DER ECDSA (r,s) -> raw 64-byte r||s function derToRawRS(der: Uint8Array): Uint8Array { let offset = 0 @@ -364,7 +332,7 @@ function leftPad32(bytes: Uint8Array): Uint8Array { } ``` -## 4) Generate and attach the signature extension data +## 4) Build the signature extension payload Flow’s transaction signature extensions allow additional data to be provided alongside a signature so verifiers can reconstruct and validate the authenticator’s signed message. For WebAuthn, this typically includes: @@ -406,7 +374,6 @@ Reference: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/ ## Where to go next -- Implement account‑proof using `AppUtils.verifyAccountProof`: `../../build/tools/clients/fcl-js/proving-authentication.mdx` - Review signing flows and signature roles: `../../build/cadence/basics/transactions.md` - Review account key registration details: `../../build/cadence/basics/accounts.md` - Track the FLIP for any updates: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) From cbb0d053466212dbc12d3ef18f02939a6c01a3ba Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sat, 4 Oct 2025 06:58:23 -0700 Subject: [PATCH 04/19] cleanup tutorial --- .../integrations/passkeys/index.md | 84 +++++++------------ 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md index 1caa460e00..2c114d2a56 100644 --- a/docs/blockchain-development-tutorials/integrations/passkeys/index.md +++ b/docs/blockchain-development-tutorials/integrations/passkeys/index.md @@ -4,11 +4,6 @@ description: Implement passkeys on Flow using WebAuthn, covering key extraction, sidebar_position: 3 keywords: - passkeys - - WebAuthn - - FCL - - signature extension - - ECDSA_P256 - - SHA2_256 --- # Passkeys (WebAuthn) on Flow — Wallet Implementation Guide @@ -18,7 +13,7 @@ This is a wallet‑centric guide (per the FLIP) that covers end‑to‑end WebAu 1. Create a user passkey wallet 2. Sign a transaction with the user’s passkey 3. Convert and attach the signature -4. Build the signature extension payload +3. Convert and attach the signature (incl. signature extension) It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. @@ -29,9 +24,8 @@ It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cit After completing this guide, you'll be able to: - Register a WebAuthn credential and derive a Flow‑compatible public key -- Set wallet‑mode challenges per FLIP (constant for registration, payload hash for signing) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format -- Attach WebAuthn signature extension data per the FLIP and submit a Flow transaction +- Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) +- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension ## Prerequisites @@ -217,7 +211,7 @@ const { authenticatorData, clientDataJSON, signature } = assertion.response as AuthenticatorAssertionResponse ``` -### 3) Convert and attach the signature +### 3) Convert and attach the signature (incl. signature extension) WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). @@ -231,7 +225,6 @@ Convert the DER signature to Flow raw r||s and build `signatureExtension = 0x01 ```tsx import { encode as rlpEncode } from 'rlp' -import { AppUtils } from '@onflow/fcl' import { bytesToHex } from '@noble/hashes/utils' // Inputs from previous steps @@ -260,34 +253,7 @@ const flowSignature = { } ``` -Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than traditional WebAuthn server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). - -Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits, however a backend is not explicitly required. - -### Allowed algorithms for WebAuthn credentials - -Restrict `pubKeyCredParams` to algorithms supported by Flow accounts: - -```tsx -// ES256 (P-256 + SHA-256) is recommended and maps to ECDSA_P256/SHA2_256 on Flow -pubKeyCredParams: [ - { type: "public-key", alg: -7 }, // ES256 - // Optionally, if you support secp256k1 keys as Flow account keys: - // { type: "public-key", alg: -47 }, // ES256K (secp256k1) -] -``` - -Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. - -## Appendix: Helpers - -> The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. - -Useful references: - -- Transactions/signatures: `../../build/cadence/basics/transactions.md` -- User signatures via FCL: `../../build/tools/clients/fcl-js/user-signatures.md` -- FLIP spec: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) +TODO INCLUDE FCL INFO Helpers used above: @@ -332,28 +298,36 @@ function leftPad32(bytes: Uint8Array): Uint8Array { } ``` -## 4) Build the signature extension payload +Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than traditional WebAuthn server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). -Flow’s transaction signature extensions allow additional data to be provided alongside a signature so verifiers can reconstruct and validate the authenticator’s signed message. For WebAuthn, this typically includes: +Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits, however a backend is not explicitly required. -- `clientDataJSON` (base64url) -- `authenticatorData` (base64url) -- The authenticator’s signature (converted to raw `r||s` for Flow) -- Any FLIP‑required metadata (e.g., `type`, `rpIdHash`, `origin` semantics) +### Allowed algorithms for WebAuthn credentials -High‑level steps: +Restrict `pubKeyCredParams` to algorithms supported by Flow accounts: + +```tsx +// ES256 (P-256 + SHA-256) is recommended and maps to ECDSA_P256/SHA2_256 on Flow +pubKeyCredParams: [ + { type: "public-key", alg: -7 }, // ES256 + // Optionally, if you support secp256k1 keys as Flow account keys: + // { type: "public-key", alg: -47 }, // ES256K (secp256k1) +] +``` -1. After `navigator.credentials.get`, collect: - - `assertion.response.clientDataJSON` - - `assertion.response.authenticatorData` - - `assertion.response.signature` -2. Encode `clientDataJSON` and `authenticatorData` as base64url. -3. Convert the DER signature to Flow raw `r||s`. -4. Attach the extension structure specified in the FLIP to the transaction signature so nodes/verifiers can validate according to WebAuthn rules. +Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. -> The exact extension schema and how it is serialized into the transaction is defined in the FLIP. Implementations should follow the FLIP verbatim to remain compatible with network verification. +## Appendix: Helpers + +> The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. + +Useful references: + +- Transactions/signatures: `../../build/cadence/basics/transactions.md` +- User signatures via FCL: `../../build/tools/clients/fcl-js/user-signatures.md` +- FLIP spec: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) -Reference: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) + ## Notes from the PoC From f7a0ce4d4c0799a464fb12a1e952602ba8a0445d Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 6 Oct 2025 09:17:25 -0700 Subject: [PATCH 05/19] cleanup draft organization --- .../integrations/passkeys/index.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md index 2c114d2a56..185a7b4a36 100644 --- a/docs/blockchain-development-tutorials/integrations/passkeys/index.md +++ b/docs/blockchain-development-tutorials/integrations/passkeys/index.md @@ -12,7 +12,6 @@ This is a wallet‑centric guide (per the FLIP) that covers end‑to‑end WebAu 1. Create a user passkey wallet 2. Sign a transaction with the user’s passkey -3. Convert and attach the signature 3. Convert and attach the signature (incl. signature extension) It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. @@ -148,7 +147,7 @@ const cosePubKey = extractCosePublicKeyFromAttestation(attObj) const publicKeySec1Hex = coseEcP256ToSec1UncompressedHex(cosePubKey) ``` -## 2) Sign a transaction with passkey wallet (WebAuthn) +## 2) Sign a transaction with the user’s passkey ### Generate the challenge @@ -211,7 +210,7 @@ const { authenticatorData, clientDataJSON, signature } = assertion.response as AuthenticatorAssertionResponse ``` -### 3) Convert and attach the signature (incl. signature extension) +## 3) Convert and attach the signature (incl. signature extension) WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). @@ -253,8 +252,6 @@ const flowSignature = { } ``` -TODO INCLUDE FCL INFO - Helpers used above: ```tsx From db85703c7c9500c03e01a53f1ff9063035c351c8 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 7 Oct 2025 08:18:59 -0700 Subject: [PATCH 06/19] Move doc --- .../forte/index.md | 4 + .../forte/passkeys/index.md | 417 ++++++++++++++++++ yarn.lock | 161 ------- 3 files changed, 421 insertions(+), 161 deletions(-) create mode 100644 docs/blockchain-development-tutorials/forte/passkeys/index.md diff --git a/docs/blockchain-development-tutorials/forte/index.md b/docs/blockchain-development-tutorials/forte/index.md index 7b881a3feb..3113f12483 100644 --- a/docs/blockchain-development-tutorials/forte/index.md +++ b/docs/blockchain-development-tutorials/forte/index.md @@ -33,6 +33,9 @@ The Forte network upgrade introduces several groundbreaking features that expand Learn how to build decentralized finance applications using the Flow Actions framework, enabling developers to create composable DeFi workflows. Flow Actions provide standardized interfaces that make it easy to combine different DeFi protocols and create sophisticated financial applications. ### [Scheduled Transactions] +### [Passkeys (WebAuthn)] + +Implement device-backed passkeys using the Web Authentication API to register Flow account keys and sign transactions with secure, user-friendly authentication. Discover how to implement scheduled transactions for time-based smart contract execution on Flow. These tutorials cover creating automated workflows, cron-like functionality, and time-sensitive blockchain applications that can execute without manual intervention. @@ -58,5 +61,6 @@ The Forte network upgrade represents a significant evolution of Flow's capabilit [Flow Actions]: ./flow-actions/index.md [Scheduled Transactions]: ./scheduled-transactions/index.md +[Passkeys (WebAuthn)]: ./passkeys/index.md [Introduction to Flow Actions]: ./flow-actions/intro-to-flow-actions.md [Scheduled Transactions Introduction]: ./scheduled-transactions/scheduled-transactions-introduction.md diff --git a/docs/blockchain-development-tutorials/forte/passkeys/index.md b/docs/blockchain-development-tutorials/forte/passkeys/index.md new file mode 100644 index 0000000000..7ad1e8e3d4 --- /dev/null +++ b/docs/blockchain-development-tutorials/forte/passkeys/index.md @@ -0,0 +1,417 @@ +--- +title: Passkeys (WebAuthn) +description: Implement passkeys on Flow using WebAuthn, covering key extraction, challenges, signature formatting for Flow, and signature extensions. +sidebar_position: 5 +keywords: + - passkeys +--- + +# Passkeys (WebAuthn) + +This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) that covers end‑to‑end WebAuthn integration for Flow: + +1. Register a passkey and add a Flow account key +2. Sign a transaction with the user’s passkey (includes conversion, extension, and submission) + +It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. + +## What you’ll learn + +After completing this guide, you'll be able to: + +- Register a WebAuthn credential and derive a Flow‑compatible public key +- Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) +- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension + +## Prerequisites + +- Working knowledge of modern frontend (React/Next.js) and basic backend +- Familiarity with WebAuthn/Passkeys concepts and platform constraints +- FCL installed and configured for your app +- A plan for secure backend entropy (32‑byte minimum) and nonce persistence +- Flow accounts and keys: [Signature and Hash Algorithms] + + +## Registration + +When a user registers a passkey via [navigator.credentials.create()] with `{ publicKey }`, the authenticator returns an attestation containing the new credential’s public key. On Flow, you can register that public key on an account as `ECDSA_P256` or `ECDSA_secp256k1`. This guide demonstrates `ECDSA_P256` paired with `SHA2_256` hashing. + +High‑level steps: + +1. On the server, generate `PublicKeyCredentialCreationOptions` and send to the client. +2. On the client, call `navigator.credentials.create()` and return the credential to the server. +3. Verify attestation if necessary and extract the COSE public key (P‑256 in this guide). Convert it to raw uncompressed 64‑byte `X||Y` hex expected by Flow. +4. Submit a transaction to add the key to the Flow account with weight and algorithms: + - Signature algorithm: `ECDSA_P256` + - Hash algorithm: `SHA2_256` + +:::tip +Libraries like SimpleWebAuthn can parse the COSE key and produce the raw public key bytes required for onchain registration. Ensure you normalize into the exact raw byte format Flow expects before writing to the account key. +::: + +### Build creation options and create credential + +Minimum example — wallet‑mode registration (challenge can be constant per FLIP): + +This builds `PublicKeyCredentialCreationOptions` for a wallet RP with a constant registration challenge and ES256 (P‑256) so the resulting public key can be registered on a Flow account. + +```tsx +// In a wallet (RP = wallet origin). The challenge satisfies API & correlates request/response. +// Use a stable, opaque user.id per wallet user (do not randomize per request). + +const rp = { name: "Passkey Wallet", id: window.location.hostname } as const +const user = { + id: getStableUserIdBytes(), // Uint8Array (16–64 bytes) stable per user + name: "flow-user", + displayName: "Flow User", +} as const + +const creationOptions: PublicKeyCredentialCreationOptions = { + challenge: new TextEncoder().encode("flow-wallet-register"), // constant is acceptable in wallet-mode; wallet providers may choose and use a constant value as needed for correlation + rp, + user, + pubKeyCredParams: [ + { type: "public-key", alg: -7 }, // ES256 (P-256 + SHA-256) + // Optionally ES256K if you support secp256k1 Flow keys: + // { type: "public-key", alg: -47 }, + ], + authenticatorSelection: { userVerification: "preferred" }, + timeout: 60_000, + attestation: "none", +} + +const credential = await navigator.credentials.create({ publicKey: creationOptions }) + +// Send to wallet-core (or local) to extract COSE P-256 public key (verify attestation if necessary) +// Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 (this guide’s choice) +``` + +### Extract and normalize public key + +Client-side example — extract COSE public key (no verification) and derive raw uncompressed 64-byte X||Y hex suitable for Flow key registration: + +This parses the `attestationObject` to locate the COSE EC2 `credentialPublicKey`, reads the x/y coordinates, and returns raw uncompressed 64-byte `X||Y` hex suitable for Flow key registration. Attestation verification is intentionally omitted here. + +```tsx +// Uses a small CBOR decoder (e.g., 'cbor' or 'cbor-x') to parse attestationObject +import * as CBOR from 'cbor' + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') +} + +function extractCosePublicKeyFromAttestation(attObj: Uint8Array): Uint8Array { + // attestationObject is a CBOR map with 'authData' + const decoded: any = CBOR.decode(attObj) + const authData = new Uint8Array(decoded.authData) + + // Parse authData (WebAuthn spec): + // rpIdHash(32) + flags(1) + signCount(4) = 37 bytes header + let offset = 37 + // aaguid (16) + offset += 16 + // credentialId length (2 bytes, big-endian) + const credIdLen = (authData[offset] << 8) | authData[offset + 1] + offset += 2 + // credentialId (credIdLen bytes) + offset += credIdLen + // The next CBOR structure is the credentialPublicKey (COSE key) + return authData.slice(offset) +} + +function coseEcP256ToUncompressedXYHex(coseKey: Uint8Array): string { + // COSE EC2 key is a CBOR map; for P-256, x = -2, y = -3 + const m: Map = CBOR.decode(coseKey) + const x = new Uint8Array(m.get(-2)) + const y = new Uint8Array(m.get(-3)) + if (x.length !== 32 || y.length !== 32) throw new Error('Invalid P-256 coordinate lengths') + const xy = new Uint8Array(64) + xy.set(x, 0) + xy.set(y, 32) + return toHex(xy) // 64-byte X||Y hex, no 0x or 0x04 prefix +} + +// Usage +const cred = (await navigator.credentials.create({ publicKey: creationOptions })) as PublicKeyCredential +const att = cred.response as AuthenticatorAttestationResponse +const attObj = new Uint8Array(att.attestationObject as ArrayBuffer) +const cosePubKey = extractCosePublicKeyFromAttestation(attObj) +const publicKeyHex = coseEcP256ToUncompressedXYHex(cosePubKey) +``` + + + +### Add key to account + +Now that you have the user's public key, provision a Flow account with that key. Creating accounts requires payment; in practice, account instantiation typically occurs on the wallet provider's backend service. + +In the PoC demo, we used a test API to provision an account with the public key: + +```ts +const ACCOUNT_API = "https://wallet.example.com/api/accounts/provision" + +export async function createAccountWithPublicKey( + publicKeyHex: string, + _opts?: {signAlgo?: number; hashAlgo?: number; weight?: number} +): Promise { + const trimmed = publicKeyHex + const body: ProvisionAccountRequest = { + publicKey: trimmed, + signatureAlgorithm: "ECDSA_P256", + hashAlgorithm: "SHA2_256", + } + const res = await fetch(ACCOUNT_API, { + method: "POST", + headers: {Accept: "application/json", "Content-Type": "application/json"}, + body: JSON.stringify(body), + }) + if (!res.ok) throw new Error(`Account API error: ${res.status}`) + const json = (await res.json()) as ProvisionAccountResponse + if (!json?.address) throw new Error("Account API missing address in response") + return json.address +} +``` + +:::note +In production, this would be a service owned by the wallet provider that creates the account and attaches the user's public key, for reasons outlined in [WebAuthn Credential Support (FLIP)] (e.g., payment handling, abuse prevention, telemetry, and correlation as needed). +::: + +## Signing + +### Generate the challenge + +- Assertion (transaction signing): Wallet sets `challenge` to the SHA2‑256 of the signable transaction message (payload or envelope per signer role). No server‑sent challenge is used. Flow includes a domain‑separation tag in the signable bytes. + +Minimal example — derive signable message and hash (per FLIP): + +Compute the signer‑specific signable message and hash it with SHA2‑256 to produce the WebAuthn `challenge` (no server‑generated nonce is used in wallet mode). + +```tsx +// Imports for helpers used to build the signable message +import { encodeMessageFromSignable, encodeTransactionPayload } from '@onflow/fcl' +// Hash/encoding utilities (example libs) +import { sha256 } from '@noble/hashes/sha256' +import { hexToBytes } from '@noble/hashes/utils' + +// Inputs: +// - signable: object containing the voucher/payload bytes (e.g., from a ready payload) +// - address: the signing account address (hex string) + +declare const signable: any +declare const address: string + +// 1) Encode the signable message for this signer (payload vs envelope) +const msgHex = encodeMessageFromSignable(signable, address) +const payloadMsgHex = encodeTransactionPayload(signable.voucher) +const role = msgHex === payloadMsgHex ? "payload" : "envelope" + +// 2) Compute SHA2-256(msgHex) -> 32-byte challenge (Flow keys commonly use SHA2_256) +const signableHash: Uint8Array = sha256(hexToBytes(msgHex)) + +// 3) Call navigator.credentials.get with challenge = signableHash +// (see next subsection for a full getAssertion example) +``` + +:::note +`encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑specific helpers. If you are not using FCL, construct the Flow signable transaction message yourself (payload for proposer/authorizer, envelope for payer), then compute `SHA2‑256(messageBytes)` for the challenge. The payload encoding shown here applies regardless of wallet implementation; the helper calls are simply conveniences from FCL. +::: + +### Request assertion + +Minimal example — wallet assertion: + +Build [PublicKeyCredentialRequestOptions] and request an assertion using the transaction hash as `challenge`. `rpId` must match the wallet domain. When the wallet has mapped the active account to a credential, include `allowCredentials` with that credential ID to avoid extra prompts; omitting it is permissible for discoverable credentials. You will invoke [navigator.credentials.get()]. + +```tsx +// signableHash is SHA2-256(signable message: payload or envelope) +declare const signableHash: Uint8Array +declare const credentialId: Uint8Array // Credential ID for the active account (from prior auth) + +const requestOptions: PublicKeyCredentialRequestOptions = { + challenge: signableHash, + rpId: window.location.hostname, + userVerification: "preferred", + timeout: 60_000, + allowCredentials: [ + { + type: "public-key", + id: credentialId, + }, + ], +} + +const assertion = (await navigator.credentials.get({ + publicKey: requestOptions, +})) as PublicKeyCredential + +const { authenticatorData, clientDataJSON, signature } = + assertion.response as AuthenticatorAssertionResponse +``` + +:::note +Wallets typically know which credential corresponds to the user’s active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn Credential Support (FLIP)] for wallet‑mode guidance. +::: + + + +### Convert and attach signature + +WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). + +- Convert the DER `signature` to Flow raw `r||s` (64 bytes) and attach with `addr` and `keyId`. +- Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. + +Minimal example — convert and attach for submission: + +Convert the DER signature to Flow raw r||s and build `signatureExtension = 0x01 || RLP([authenticatorData, clientDataJSON])` per the FLIP, then compose the Flow signature object for inclusion in your transaction. + +```tsx +import { encode as rlpEncode } from 'rlp' +import { bytesToHex } from '@noble/hashes/utils' + +// Inputs from previous steps +declare const address: string // 0x-prefixed Flow address +declare const keyId: number // Account key index used for signing +declare const signature: Uint8Array // DER signature from WebAuthn assertion +declare const clientDataJSON: Uint8Array +declare const authenticatorData: Uint8Array + +// 1) DER -> raw r||s (64 bytes), implementation below or similar +const rawSig = derToRawRS(signature) + +// 2) Build extension_data per FLIP: 0x01 || RLP([authenticatorData, clientDataJSON]) +const rlpPayload = rlpEncode([authenticatorData, clientDataJSON]) as Uint8Array | Buffer +const rlpBytes = rlpPayload instanceof Uint8Array ? rlpPayload : new Uint8Array(rlpPayload) +const extension_data = new Uint8Array(1 + rlpBytes.length) +extension_data[0] = 0x01 +extension_data.set(rlpBytes, 1) + +// 3) Compose Flow signature object +const flowSignature = { + addr: address, // e.g., '0x1cf0e2f2f715450' + keyId, // integer key index + signature: '0x' + bytesToHex(rawSig), + signatureExtension: extension_data, +} +``` + +#### Submit the signature + +Return the signature data to the application that initiated signing. The application should attach it to the user transaction for the signer (`addr`, `keyId`) and submit the transaction to the network. + +See [Transactions] for how signatures are attached per signer role (payload vs envelope) and how submissions are finalized. + +#### Helper: derToRawRS + +```tsx +// Minimal DER ECDSA (r,s) -> raw 64-byte r||s +function derToRawRS(der: Uint8Array): Uint8Array { + let offset = 0 + if (der[offset++] !== 0x30) throw new Error("Invalid DER sequence") + const seqLen = der[offset++] // assumes short form + if (seqLen + 2 !== der.length) throw new Error("Invalid DER length") + + if (der[offset++] !== 0x02) throw new Error("Missing r INTEGER") + const rLen = der[offset++] + let r = der.slice(offset, offset + rLen) + offset += rLen + if (der[offset++] !== 0x02) throw new Error("Missing s INTEGER") + const sLen = der[offset++] + let s = der.slice(offset, offset + sLen) + + // Strip leading zeros and left-pad to 32 bytes + r = stripLeadingZeros(r) + s = stripLeadingZeros(s) + const r32 = leftPad32(r) + const s32 = leftPad32(s) + const raw = new Uint8Array(64) + raw.set(r32, 0) + raw.set(s32, 32) + return raw +} + +function stripLeadingZeros(bytes: Uint8Array): Uint8Array { + let i = 0 + while (i < bytes.length - 1 && bytes[i] === 0x00) i++ + return bytes.slice(i) +} + +function leftPad32(bytes: Uint8Array): Uint8Array { + if (bytes.length > 32) throw new Error("Component too long") + const out = new Uint8Array(32) + out.set(bytes, 32 - bytes.length) + return out +} +``` + + + + + +## Notes from the PoC + +- The PoC in `fcl-js/packages/passkey-wallet` demonstrates end‑to‑end flows for passkey creation and assertion, including: + - Extracting and normalizing the P‑256 public key for Flow + - Generating secure nonces and verifying account‑proof + - Converting DER signatures to raw `r||s` + - Packaging WebAuthn fields as signature extension data + +> Align your implementation with the FLIP to ensure your extension payloads and verification logic match network expectations. + +## Security and UX considerations + +- Use `ECDSA_P256` with `SHA2_256` for Flow account keys derived from WebAuthn P‑256. +- Enforce nonce expiry, single‑use semantics, and strong server‑side randomness. +- Clearly communicate platform prompts and recovery paths; passkeys UX can differ across OS/browsers. + - Replay protection: Flow uses on‑chain proposal‑key sequence numbers; see [Replay attacks]. + - Optional wallet backend: store short‑lived correlation data or rate‑limits as needed (not required). + + +## Credential management (wallet responsibilities) + +Wallet providers should persist credential metadata to support seamless signing, rotation, and recovery: + +- Map `credentialId` ↔ Flow `addr` (and `keyId`) for the active account +- Store `rpId`, user handle, and (optionally) `aaguid`/attestation info for risk decisions +- Support multiple credentials per account and revocation/rotation workflows +- Enforce nonce/sequence semantics and rate limits server-side as needed + +See [WebAuthn Credential Support (FLIP)] for rationale and wallet‑mode guidance. + +## Conclusion + +In this tutorial, you integrated passkeys (WebAuthn) with Flow for both registration and signing. + +Now that you have completed the tutorial, you should be able to: + +- Register a WebAuthn credential and derive a Flow‑compatible public key +- Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) +- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension + +### Further reading + +- Review signing flows and roles: [Transactions] +- Account keys: [Signature and Hash Algorithms] +- Web Authentication API (MDN): [Web Authentication API] +- Flow Client Library (FCL): [Flow Client Library] +- Wallet Provider Spec: [Wallet Provider Spec] +- Track updates: [FLIP 264: WebAuthn Credential Support] + + +[WebAuthn Credential Support (FLIP)]: https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md +[FLIP 264: WebAuthn Credential Support]: https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md +[Web Authentication API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API +[navigator.credentials.create()]: https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create +[PublicKeyCredentialCreationOptions]: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions +[PublicKeyCredentialRequestOptions]: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions +[navigator.credentials.get()]: https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get +[PublicKeyCredential]: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential +[AuthenticatorAttestationResponse]: https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse +[AuthenticatorAssertionResponse]: https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse +[Replay attacks]: https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks +[Transactions]: ../../../build/cadence/basics/transactions.md +[Signature and Hash Algorithms]: ../../../build/cadence/basics/accounts.md +[Flow Client Library]: ../../../build/tools/clients/fcl-js/index.md +[Wallet Provider Spec]: ../../../build/tools/wallet-provider-spec/index.md + + diff --git a/yarn.lock b/yarn.lock index cbd0d9cde1..85cb931257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2638,155 +2638,6 @@ resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== -"@fluentui/date-time-utilities@^8.6.10": - version "8.6.10" - resolved "https://registry.yarnpkg.com/@fluentui/date-time-utilities/-/date-time-utilities-8.6.10.tgz#b24384c0f525b4cb798cf2e9d9855182da1e6a91" - integrity sha512-Bxq8DIMkFvkpCA1HKtCHdnFwPAnXLz3TkGp9kpi2T6VIv6VtLVSxRn95mbsUydpP9Up/DLglp/z9re5YFBGNbw== - dependencies: - "@fluentui/set-version" "^8.2.24" - tslib "^2.1.0" - -"@fluentui/dom-utilities@^2.3.10": - version "2.3.10" - resolved "https://registry.yarnpkg.com/@fluentui/dom-utilities/-/dom-utilities-2.3.10.tgz#556c4387c2febdf7f7f63b38e30d4f6edd40e816" - integrity sha512-6WDImiLqTOpkEtfUKSStcTDpzmJfL6ZammomcjawN9xH/8u8G3Hx72CIt2MNck9giw/oUlNLJFdWRAjeP3rmPQ== - dependencies: - "@fluentui/set-version" "^8.2.24" - tslib "^2.1.0" - -"@fluentui/font-icons-mdl2@^8.5.63": - version "8.5.63" - resolved "https://registry.yarnpkg.com/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.63.tgz#334e05776dc97a13ea78f8931869bbdfdfe19fb9" - integrity sha512-aSwI7GiIICvzrzok8xV082U1U8oieyjELUFjyocC12/WoAYUdLJAk3L4nh4o4WaFgpfHmbtBul7GIeJBDQxaHA== - dependencies: - "@fluentui/set-version" "^8.2.24" - "@fluentui/style-utilities" "^8.12.4" - "@fluentui/utilities" "^8.15.23" - tslib "^2.1.0" - -"@fluentui/foundation-legacy@^8.4.30": - version "8.4.30" - resolved "https://registry.yarnpkg.com/@fluentui/foundation-legacy/-/foundation-legacy-8.4.30.tgz#fcf9fd117f7ae045e75063c8d1f1fd1889ac9bf1" - integrity sha512-BQv7CqILjI+xKMZOOrs01EnuMeRBecmDRze2ThuxB8vZx+xTmBgDlrRIHnWDFIqfC6dMvNCnBwu6ubgiPk8ybQ== - dependencies: - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/set-version" "^8.2.24" - "@fluentui/style-utilities" "^8.12.4" - "@fluentui/utilities" "^8.15.23" - tslib "^2.1.0" - -"@fluentui/keyboard-key@^0.4.23": - version "0.4.23" - resolved "https://registry.yarnpkg.com/@fluentui/keyboard-key/-/keyboard-key-0.4.23.tgz#ebaa87b1dcdfb2a9ac805c678f9520e9d9194c3c" - integrity sha512-9GXeyUqNJUdg5JiQUZeGPiKnRzMRi9YEUn1l9zq6X/imYdMhxHrxpVZS12129cBfgvPyxt9ceJpywSfmLWqlKA== - dependencies: - tslib "^2.1.0" - -"@fluentui/merge-styles@^8.6.14": - version "8.6.14" - resolved "https://registry.yarnpkg.com/@fluentui/merge-styles/-/merge-styles-8.6.14.tgz#92482122cbdf4c35a3607f7939aae2027d908d50" - integrity sha512-vghuHFAfQgS9WLIIs4kgDOCh/DHd5vGIddP4/bzposhlAVLZR6wUBqldm9AuCdY88r5LyCRMavVJLV+Up3xdvA== - dependencies: - "@fluentui/set-version" "^8.2.24" - tslib "^2.1.0" - -"@fluentui/react-focus@^8.9.26": - version "8.9.26" - resolved "https://registry.yarnpkg.com/@fluentui/react-focus/-/react-focus-8.9.26.tgz#26bb1ea7692cd9b73d33b9b9c63cd8c4beb426b3" - integrity sha512-mbFsdCSSgpxTxw3dCIek7hmTubxJZT4+7uFRZOo0abKKCn6rWGcQWpiLT9ndJrD6I5TtEzJaw68WEk7kvFYyAw== - dependencies: - "@fluentui/keyboard-key" "^0.4.23" - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/set-version" "^8.2.24" - "@fluentui/style-utilities" "^8.12.4" - "@fluentui/utilities" "^8.15.23" - tslib "^2.1.0" - -"@fluentui/react-hooks@^8.8.19": - version "8.8.19" - resolved "https://registry.yarnpkg.com/@fluentui/react-hooks/-/react-hooks-8.8.19.tgz#62572757d00d6ab9b0b249308ef45bdcd54fd360" - integrity sha512-uXcETVTl2L0G/Ocyb2Rjym96tcJd2NaZ2Hqt6EJcBb9KJD9irNeXjCCxsRNPC5kBDbfrQML2aai+M2kU9lZKNQ== - dependencies: - "@fluentui/react-window-provider" "^2.2.30" - "@fluentui/set-version" "^8.2.24" - "@fluentui/utilities" "^8.15.22" - tslib "^2.1.0" - -"@fluentui/react-portal-compat-context@^9.0.14": - version "9.0.14" - resolved "https://registry.yarnpkg.com/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.14.tgz#b8a3105001562cd7977da267c77600f2b3331df8" - integrity sha512-OyAaHIbkwqlIgStd8qnS2gcZMmrNokesCUnmELzaygTHJ8Q4L9BPlah98b0wRFf5Sdeu1EPT6nDgn3VuLGqSnQ== - dependencies: - "@swc/helpers" "^0.5.1" - -"@fluentui/react-window-provider@^2.2.30": - version "2.2.30" - resolved "https://registry.yarnpkg.com/@fluentui/react-window-provider/-/react-window-provider-2.2.30.tgz#4e2c3a524a908cf6241318b54f428fbc2248a5ab" - integrity sha512-2SXuiZcU29W0D9zfExcTfzVx97OI50YCn5fGGO0bTDuP5VxzTQp1mipAY4qm/yJMMinoXkzBGLl1rK0Tdtxh1w== - dependencies: - "@fluentui/set-version" "^8.2.24" - tslib "^2.1.0" - -"@fluentui/react@^8.123.5": - version "8.123.6" - resolved "https://registry.yarnpkg.com/@fluentui/react/-/react-8.123.6.tgz#22b7f4c3466f6e97d598eb3831e63ba2dd1bb61e" - integrity sha512-z/y2Zqz3oeqb9VtWAtkvwt1yJ0TCJgqgU8hTGEUl6SuwXZqb0jTJamXXfB7T1DlSOkf2mTXaMU4alXX/odaONQ== - dependencies: - "@fluentui/date-time-utilities" "^8.6.10" - "@fluentui/font-icons-mdl2" "^8.5.63" - "@fluentui/foundation-legacy" "^8.4.30" - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/react-focus" "^8.9.26" - "@fluentui/react-hooks" "^8.8.19" - "@fluentui/react-portal-compat-context" "^9.0.14" - "@fluentui/react-window-provider" "^2.2.30" - "@fluentui/set-version" "^8.2.24" - "@fluentui/style-utilities" "^8.12.4" - "@fluentui/theme" "^2.6.67" - "@fluentui/utilities" "^8.15.23" - "@microsoft/load-themed-styles" "^1.10.26" - tslib "^2.1.0" - -"@fluentui/set-version@^8.2.24": - version "8.2.24" - resolved "https://registry.yarnpkg.com/@fluentui/set-version/-/set-version-8.2.24.tgz#530d09eb4385bb298dd85a877cc492354fe93377" - integrity sha512-8uNi2ThvNgF+6d3q2luFVVdk/wZV0AbRfJ85kkvf2+oSRY+f6QVK0w13vMorNhA5puumKcZniZoAfUF02w7NSg== - dependencies: - tslib "^2.1.0" - -"@fluentui/style-utilities@^8.12.4": - version "8.12.4" - resolved "https://registry.yarnpkg.com/@fluentui/style-utilities/-/style-utilities-8.12.4.tgz#94ec45d1f25aa5e14864ef284b87cae0003927cf" - integrity sha512-s+u9v5gQ1SMLsuI7pUP2rXo6Kd2v+X2l4eKQxKO4UO0dZv/kLaA4fnmkXaIDpKkLS+eI9KBAG/W2KhojwBSt1g== - dependencies: - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/set-version" "^8.2.24" - "@fluentui/theme" "^2.6.67" - "@fluentui/utilities" "^8.15.23" - "@microsoft/load-themed-styles" "^1.10.26" - tslib "^2.1.0" - -"@fluentui/theme@^2.6.67": - version "2.6.67" - resolved "https://registry.yarnpkg.com/@fluentui/theme/-/theme-2.6.67.tgz#0efd0a6516e6f858072859fe811d592f8904093e" - integrity sha512-+9+VkIkZ+NCQDXFP6+WV2ChAj/KHphOEDCvGO15w8ql7sqRxeRQACtoWYNq1tAAsodbnq/amCfo2PNy2VIcIOQ== - dependencies: - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/set-version" "^8.2.24" - "@fluentui/utilities" "^8.15.22" - tslib "^2.1.0" - -"@fluentui/utilities@^8.15.22", "@fluentui/utilities@^8.15.23": - version "8.15.23" - resolved "https://registry.yarnpkg.com/@fluentui/utilities/-/utilities-8.15.23.tgz#38f1832d6e75bd92434b3619f516efde31e9f222" - integrity sha512-Vrto/daJ8lhwAZqf5XkC3qi+4PW8Dk2X0yXNgia9aV2wxsbMoEikqC08cNQnapTaBLkurAIShTbR56mxKh8Kyw== - dependencies: - "@fluentui/dom-utilities" "^2.3.10" - "@fluentui/merge-styles" "^8.6.14" - "@fluentui/react-window-provider" "^2.2.30" - "@fluentui/set-version" "^8.2.24" - tslib "^2.1.0" - "@fortawesome/fontawesome-common-types@6.7.2": version "6.7.2" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz" @@ -3329,11 +3180,6 @@ dependencies: moo "^0.5.1" -"@microsoft/load-themed-styles@^1.10.26": - version "1.10.295" - resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz#d3c8d7ab186f422727ba112d6ebe5fe8e41051d9" - integrity sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg== - "@module-federation/error-codes@0.18.0": version "0.18.0" resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.18.0.tgz#00830ece3b5b6bcda0a874a8426bcd94599bf738" @@ -5089,13 +4935,6 @@ dependencies: tslib "^2.8.0" -"@swc/helpers@^0.5.1": - version "0.5.17" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.17.tgz#5a7be95ac0f0bf186e7e6e890e7a6f6cda6ce971" - integrity sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A== - dependencies: - tslib "^2.8.0" - "@swc/html-darwin-arm64@1.13.20": version "1.13.20" resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.13.20.tgz#e1c3bf132e86d55ec6cf12e22c68c3d833247ece" From 9eb468e55be6d8f4dc66fa88ce4af96b10193116 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 7 Oct 2025 08:19:05 -0700 Subject: [PATCH 07/19] move doc --- .../forte/index.md | 5 +- .../integrations/passkeys/index.md | 352 ------------------ 2 files changed, 3 insertions(+), 354 deletions(-) delete mode 100644 docs/blockchain-development-tutorials/integrations/passkeys/index.md diff --git a/docs/blockchain-development-tutorials/forte/index.md b/docs/blockchain-development-tutorials/forte/index.md index 3113f12483..0220f423ef 100644 --- a/docs/blockchain-development-tutorials/forte/index.md +++ b/docs/blockchain-development-tutorials/forte/index.md @@ -33,12 +33,13 @@ The Forte network upgrade introduces several groundbreaking features that expand Learn how to build decentralized finance applications using the Flow Actions framework, enabling developers to create composable DeFi workflows. Flow Actions provide standardized interfaces that make it easy to combine different DeFi protocols and create sophisticated financial applications. ### [Scheduled Transactions] + +Discover how to implement scheduled transactions for time-based smart contract execution on Flow. These tutorials cover creating automated workflows, cron-like functionality, and time-sensitive blockchain applications that can execute without manual intervention. + ### [Passkeys (WebAuthn)] Implement device-backed passkeys using the Web Authentication API to register Flow account keys and sign transactions with secure, user-friendly authentication. -Discover how to implement scheduled transactions for time-based smart contract execution on Flow. These tutorials cover creating automated workflows, cron-like functionality, and time-sensitive blockchain applications that can execute without manual intervention. - ## Getting Started To begin with Forte tutorials, we recommend starting with: diff --git a/docs/blockchain-development-tutorials/integrations/passkeys/index.md b/docs/blockchain-development-tutorials/integrations/passkeys/index.md deleted file mode 100644 index 185a7b4a36..0000000000 --- a/docs/blockchain-development-tutorials/integrations/passkeys/index.md +++ /dev/null @@ -1,352 +0,0 @@ ---- -title: Passkeys (WebAuthn) on Flow — Registration and Signing -description: Implement passkeys on Flow using WebAuthn, covering key extraction, challenges, signature formatting for Flow, and signature extensions. -sidebar_position: 3 -keywords: - - passkeys ---- - -# Passkeys (WebAuthn) on Flow — Wallet Implementation Guide - -This is a wallet‑centric guide (per the FLIP) that covers end‑to‑end WebAuthn integration for Flow: - -1. Create a user passkey wallet -2. Sign a transaction with the user’s passkey -3. Convert and attach the signature (incl. signature extension) - -It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. - -> PoC source: `/Users/jribbink/repos/fcl-js/packages/passkey-wallet` - -## What you’ll learn - -After completing this guide, you'll be able to: - -- Register a WebAuthn credential and derive a Flow‑compatible public key -- Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension - -## Prerequisites - -- Working knowledge of modern frontend (React/Next.js) and basic backend -- Familiarity with WebAuthn/Passkeys concepts and platform constraints -- FCL installed and configured for your app -- A plan for secure backend entropy (32‑byte minimum) and nonce persistence - -See also - -- Transactions and signatures on Flow: `../../build/cadence/basics/transactions.md` -- Account keys, signature and hash algorithms: `../../build/cadence/basics/accounts.md` - -External references - -- WebAuthn credential support FLIP: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) -- Web Authentication API (MDN): [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) - -## 1) Create a user passkey wallet - -When a user registers a passkey via `navigator.credentials.create({ publicKey })`, the authenticator returns an attestation containing the new credential’s public key. For Flow, you’ll register that public key on an account as an `ECDSA_P256` key paired with `SHA2_256` hashing. - -High‑level steps: - -1. On the server, generate `PublicKeyCredentialCreationOptions` and send to the client. -2. On the client, call WebAuthn `create()` and return the credential to the server. -3. Verify attestation if necessary and extract the COSE public key (P‑256). Convert it to the raw uncompressed X9.62 point format if needed and to hex bytes expected by Flow. -4. Submit a transaction to add the key to the Flow account with weight and algorithms: - - Signature algorithm: `ECDSA_P256` - - Hash algorithm: `SHA2_256` - -Key algorithm references: `../../build/cadence/basics/accounts.md` (Signature and Hash Algorithms). - -> Tip: Libraries like SimpleWebAuthn can parse the COSE key and produce the raw public key bytes required for onchain registration. Ensure you normalize into the exact raw byte format Flow expects before writing to the account key. - -Minimum example — wallet‑mode registration (challenge can be constant per FLIP): - -This builds `PublicKeyCredentialCreationOptions` for a wallet RP with a constant registration challenge and ES256 (P‑256) so the resulting public key can be registered on a Flow account. - -```tsx -// In a wallet (RP = wallet origin). The challenge satisfies API & correlates request/response. -// Use a stable, opaque user.id per wallet user (do not randomize per request). - -const rp = { name: "Passkey Wallet", id: window.location.hostname } as const -const user = { - id: getStableUserIdBytes(), // Uint8Array (16–64 bytes) stable per user - name: "flow-user", - displayName: "Flow User", -} as const - -const creationOptions: PublicKeyCredentialCreationOptions = { - challenge: new TextEncoder().encode("flow-wallet-register"), // constant is acceptable in wallet-mode; wallet providers may choose and use a constant value as needed for correlation - rp, - user, - pubKeyCredParams: [ - { type: "public-key", alg: -7 }, // ES256 (P-256 + SHA-256) - // Optionally ES256K if you support secp256k1 Flow keys: - // { type: "public-key", alg: -47 }, - ], - authenticatorSelection: { userVerification: "preferred" }, - timeout: 60_000, - attestation: "none", -} - -const credential = await navigator.credentials.create({ publicKey: creationOptions }) - -// Send to wallet-core (or local) to extract COSE P-256 public key (verify attestation if necessary) -// Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 -``` - -Client-side example — extract COSE public key (no verification) and derive SEC1 uncompressed hex: - -This parses the `attestationObject` to locate the COSE EC2 `credentialPublicKey`, reads the x/y coordinates, and returns SEC1 uncompressed bytes (0x04 || X || Y) suitable for Flow key registration. Attestation verification is intentionally omitted here. - -```tsx -// Uses a small CBOR decoder (e.g., 'cbor' or 'cbor-x') to parse attestationObject -import * as CBOR from 'cbor' - -function toHex(bytes: Uint8Array): string { - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') -} - -function extractCosePublicKeyFromAttestation(attObj: Uint8Array): Uint8Array { - // attestationObject is a CBOR map with 'authData' - const decoded: any = CBOR.decode(attObj) - const authData = new Uint8Array(decoded.authData) - - // Parse authData (WebAuthn spec): - // rpIdHash(32) + flags(1) + signCount(4) = 37 bytes header - let offset = 37 - // aaguid (16) - offset += 16 - // credentialId length (2 bytes, big-endian) - const credIdLen = (authData[offset] << 8) | authData[offset + 1] - offset += 2 - // credentialId (credIdLen bytes) - offset += credIdLen - // The next CBOR structure is the credentialPublicKey (COSE key) - return authData.slice(offset) -} - -function coseEcP256ToSec1UncompressedHex(coseKey: Uint8Array): string { - // COSE EC2 key is a CBOR map; for P-256, x = -2, y = -3 - const m: Map = CBOR.decode(coseKey) - const x = new Uint8Array(m.get(-2)) - const y = new Uint8Array(m.get(-3)) - if (x.length !== 32 || y.length !== 32) throw new Error('Invalid P-256 coordinate lengths') - const sec1 = new Uint8Array(65) - sec1[0] = 0x04 - sec1.set(x, 1) - sec1.set(y, 33) - return '0x' + toHex(sec1) -} - -// Usage -const cred = (await navigator.credentials.create({ publicKey: creationOptions })) as PublicKeyCredential -const att = cred.response as AuthenticatorAttestationResponse -const attObj = new Uint8Array(att.attestationObject as ArrayBuffer) -const cosePubKey = extractCosePublicKeyFromAttestation(attObj) -const publicKeySec1Hex = coseEcP256ToSec1UncompressedHex(cosePubKey) -``` - -## 2) Sign a transaction with the user’s passkey - -### Generate the challenge - -- Assertion (transaction signing): Wallet sets `challenge` to the SHA2‑256 of the signable transaction message (payload or envelope per signer role). No server‑sent challenge is used. Flow includes a domain‑separation tag in the signable bytes. - -Minimal example — derive signable message and hash (per FLIP): - -Compute the signer‑specific signable message and hash it with SHA2‑256 to produce the WebAuthn `challenge` (no server‑generated nonce is used in wallet mode). - -```tsx -// Imports for helpers used to build the signable message -import { encodeMessageFromSignable, encodeTransactionPayload } from '@onflow/fcl' -// Hash/encoding utilities (example libs) -import { sha256 } from '@noble/hashes/sha256' -import { hexToBytes } from '@noble/hashes/utils' - -// Inputs: -// - signable: object containing the voucher/payload bytes (e.g., from a ready payload) -// - address: the signing account address (hex string) - -declare const signable: any -declare const address: string - -// 1) Encode the signable message for this signer (payload vs envelope) -const msgHex = encodeMessageFromSignable(signable, address) -const payloadMsgHex = encodeTransactionPayload(signable.voucher) -const role = msgHex === payloadMsgHex ? "payload" : "envelope" - -// 2) Compute SHA2-256(msgHex) -> 32-byte challenge (Flow keys commonly use SHA2_256) -const signableHash: Uint8Array = sha256(hexToBytes(msgHex)) - -// 3) Call navigator.credentials.get with challenge = signableHash -// (see next subsection for a full getAssertion example) -``` - -Note: `encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑specific helpers. If you are not using FCL, construct the Flow signable transaction message yourself (payload for proposer/authorizer, envelope for payer), then compute `SHA2‑256(messageBytes)` for the challenge. The payload encoding shown here applies regardless of wallet implementation; the helper calls are simply conveniences from FCL. - -### Sign with the user's passkey - -Minimal example — wallet assertion: - -Request an assertion using the transaction hash as `challenge`. `rpId` must match the wallet domain; `allowCredentials` may be omitted for discoverable credentials. - -```tsx -// signableHash is SHA2-256(signable message: payload or envelope) -declare const signableHash: Uint8Array - -const requestOptions: PublicKeyCredentialRequestOptions = { - challenge: signableHash, - rpId: window.location.hostname, - userVerification: "preferred", - timeout: 60_000, -} - -const assertion = (await navigator.credentials.get({ - publicKey: requestOptions, -})) as PublicKeyCredential - -const { authenticatorData, clientDataJSON, signature } = - assertion.response as AuthenticatorAssertionResponse -``` - -## 3) Convert and attach the signature (incl. signature extension) - -WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). - -- Convert the DER `signature` to Flow raw `r||s` (64 bytes) and attach with `addr` and `keyId`. -- Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. -- See details below in sections 3 and 4. - -Minimal example — convert and attach for submission: - -Convert the DER signature to Flow raw r||s and build `signatureExtension = 0x01 || RLP([authenticatorData, clientDataJSON])` per the FLIP, then compose the Flow signature object for inclusion in your transaction. - -```tsx -import { encode as rlpEncode } from 'rlp' -import { bytesToHex } from '@noble/hashes/utils' - -// Inputs from previous steps -declare const address: string // 0x-prefixed Flow address -declare const keyId: number // Account key index used for signing -declare const signature: Uint8Array // DER signature from WebAuthn assertion -declare const clientDataJSON: Uint8Array -declare const authenticatorData: Uint8Array - -// 1) DER -> raw r||s (64 bytes) -const rawSig = derToRawRS(signature) - -// 2) Build extension_data per FLIP: 0x01 || RLP([authenticatorData, clientDataJSON]) -const rlpPayload = rlpEncode([authenticatorData, clientDataJSON]) as Uint8Array | Buffer -const rlpBytes = rlpPayload instanceof Uint8Array ? rlpPayload : new Uint8Array(rlpPayload) -const extension_data = new Uint8Array(1 + rlpBytes.length) -extension_data[0] = 0x01 -extension_data.set(rlpBytes, 1) - -// 3) Compose Flow signature object -const flowSignature = { - addr: address, // e.g., '0x1cf0e2f2f715450' - keyId, // integer key index - signature: '0x' + bytesToHex(rawSig), - signatureExtension: extension_data, -} -``` - -Helpers used above: - -```tsx -// Minimal DER ECDSA (r,s) -> raw 64-byte r||s -function derToRawRS(der: Uint8Array): Uint8Array { - let offset = 0 - if (der[offset++] !== 0x30) throw new Error("Invalid DER sequence") - const seqLen = der[offset++] // assumes short form - if (seqLen + 2 !== der.length) throw new Error("Invalid DER length") - - if (der[offset++] !== 0x02) throw new Error("Missing r INTEGER") - const rLen = der[offset++] - let r = der.slice(offset, offset + rLen) - offset += rLen - if (der[offset++] !== 0x02) throw new Error("Missing s INTEGER") - const sLen = der[offset++] - let s = der.slice(offset, offset + sLen) - - // Strip leading zeros and left-pad to 32 bytes - r = stripLeadingZeros(r) - s = stripLeadingZeros(s) - const r32 = leftPad32(r) - const s32 = leftPad32(s) - const raw = new Uint8Array(64) - raw.set(r32, 0) - raw.set(s32, 32) - return raw -} - -function stripLeadingZeros(bytes: Uint8Array): Uint8Array { - let i = 0 - while (i < bytes.length - 1 && bytes[i] === 0x00) i++ - return bytes.slice(i) -} - -function leftPad32(bytes: Uint8Array): Uint8Array { - if (bytes.length > 32) throw new Error("Component too long") - const out = new Uint8Array(32) - out.set(bytes, 32 - bytes.length) - return out -} -``` - -Replay protection: Flow uses on‑chain proposal‑key sequence numbers (increment per signed tx) rather than traditional WebAuthn server counters or random challenges. Details and caveats: [Replay attacks](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks). - -Optional wallet backend: You may store short‑lived correlation data (e.g., request IDs) for telemetry/rate‑limits, however a backend is not explicitly required. - -### Allowed algorithms for WebAuthn credentials - -Restrict `pubKeyCredParams` to algorithms supported by Flow accounts: - -```tsx -// ES256 (P-256 + SHA-256) is recommended and maps to ECDSA_P256/SHA2_256 on Flow -pubKeyCredParams: [ - { type: "public-key", alg: -7 }, // ES256 - // Optionally, if you support secp256k1 keys as Flow account keys: - // { type: "public-key", alg: -47 }, // ES256K (secp256k1) -] -``` - -Avoid including `RS256` (`alg: -257`) as it does not map to Flow account keys. - -## Appendix: Helpers - -> The mapping from Flow’s payload to the authenticator’s signed bytes is defined by the signature extension. Follow the FLIP for exactly how the challenge and additional fields (e.g., `clientDataJSON`, `authenticatorData`) must be constructed and later verified by Flow. - -Useful references: - -- Transactions/signatures: `../../build/cadence/basics/transactions.md` -- User signatures via FCL: `../../build/tools/clients/fcl-js/user-signatures.md` -- FLIP spec: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) - - - -## Notes from the PoC - -- The PoC in `fcl-js/packages/passkey-wallet` demonstrates end‑to‑end flows for passkey creation and assertion, including: - - Extracting and normalizing the P‑256 public key for Flow - - Generating secure nonces and verifying account‑proof - - Converting DER signatures to raw `r||s` - - Packaging WebAuthn fields as signature extension data - -> Align your implementation with the FLIP to ensure your extension payloads and verification logic match network expectations. - -## Security and UX considerations - -- Use `ECDSA_P256` with `SHA2_256` for Flow account keys derived from WebAuthn P‑256. -- Enforce nonce expiry, single‑use semantics, and strong server‑side randomness. -- Clearly communicate platform prompts and recovery paths; passkeys UX can differ across OS/browsers. -- Consider fallbacks for non‑WebAuthn environments. - -## Where to go next - -- Review signing flows and signature roles: `../../build/cadence/basics/transactions.md` -- Review account key registration details: `../../build/cadence/basics/accounts.md` -- Track the FLIP for any updates: [WebAuthn Credential Support (FLIP)](https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md) - - From 576425c1dd26871c615b744721e1467c3d3c3f98 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 7 Oct 2025 08:31:56 -0700 Subject: [PATCH 08/19] fix spacing --- docs/blockchain-development-tutorials/forte/passkeys/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blockchain-development-tutorials/forte/passkeys/index.md b/docs/blockchain-development-tutorials/forte/passkeys/index.md index 7ad1e8e3d4..8b4bb5cff0 100644 --- a/docs/blockchain-development-tutorials/forte/passkeys/index.md +++ b/docs/blockchain-development-tutorials/forte/passkeys/index.md @@ -363,8 +363,8 @@ function leftPad32(bytes: Uint8Array): Uint8Array { - Use `ECDSA_P256` with `SHA2_256` for Flow account keys derived from WebAuthn P‑256. - Enforce nonce expiry, single‑use semantics, and strong server‑side randomness. - Clearly communicate platform prompts and recovery paths; passkeys UX can differ across OS/browsers. - - Replay protection: Flow uses on‑chain proposal‑key sequence numbers; see [Replay attacks]. - - Optional wallet backend: store short‑lived correlation data or rate‑limits as needed (not required). +- Replay protection: Flow uses on‑chain proposal‑key sequence numbers; see [Replay attacks]. +- Optional wallet backend: store short‑lived correlation data or rate‑limits as needed (not required). ## Credential management (wallet responsibilities) From 1ef373cb22e7e43a7821a8db06b2941f39f9064f Mon Sep 17 00:00:00 2001 From: Tarak Ben Youssef <50252200+tarakby@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:11:44 +0800 Subject: [PATCH 09/19] passkeys docs suggestions (#1500) * clarifications * minor improvement * avoid confusing passkeys and webauthn --- .../forte/passkeys/index.md | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/docs/blockchain-development-tutorials/forte/passkeys/index.md b/docs/blockchain-development-tutorials/forte/passkeys/index.md index 8b4bb5cff0..13b3ea79e5 100644 --- a/docs/blockchain-development-tutorials/forte/passkeys/index.md +++ b/docs/blockchain-development-tutorials/forte/passkeys/index.md @@ -1,16 +1,16 @@ --- -title: Passkeys (WebAuthn) +title: Passkeys description: Implement passkeys on Flow using WebAuthn, covering key extraction, challenges, signature formatting for Flow, and signature extensions. sidebar_position: 5 keywords: - passkeys --- -# Passkeys (WebAuthn) +# Passkeys -This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) that covers end‑to‑end WebAuthn integration for Flow: +This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) that covers end‑to‑end passkeys integration for Flow: -1. Register a passkey and add a Flow account key +1. Create a passkey and add a Flow account key 2. Sign a transaction with the user’s passkey (includes conversion, extension, and submission) It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. @@ -19,28 +19,29 @@ It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cit After completing this guide, you'll be able to: -- Register a WebAuthn credential and derive a Flow‑compatible public key +- Create a passkey and derive a Flow‑compatible public key - Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension +- Convert a WebAuthn ECDSA DER signature into Flow’s raw `r||s` format and attach the transaction signature extension ## Prerequisites - Working knowledge of modern frontend (React/Next.js) and basic backend - Familiarity with WebAuthn/Passkeys concepts and platform constraints - FCL installed and configured for your app -- A plan for secure backend entropy (32‑byte minimum) and nonce persistence - Flow accounts and keys: [Signature and Hash Algorithms] - ## Registration -When a user registers a passkey via [navigator.credentials.create()] with `{ publicKey }`, the authenticator returns an attestation containing the new credential’s public key. On Flow, you can register that public key on an account as `ECDSA_P256` or `ECDSA_secp256k1`. This guide demonstrates `ECDSA_P256` paired with `SHA2_256` hashing. +When a user generates a passkey via [navigator.credentials.create()] with `{ publicKey }`, the authenticator returns an attestation containing the new credential’s public key. On Flow, you can register that public key on an account if the algorithm of the requested passkey is either `ES256` or `ES256k`. This guide demonstrates an `ES256` passkey which translates to an `ECDSA_P256` Flow key paired with `SHA2_256` hashing. Althernatively, an `ES256k` passkey translates to an `ECDSA_secp256k1` Flow key paired with `SHA2_256` hashing. High‑level steps: -1. On the server, generate `PublicKeyCredentialCreationOptions` and send to the client. -2. On the client, call `navigator.credentials.create()` and return the credential to the server. -3. Verify attestation if necessary and extract the COSE public key (P‑256 in this guide). Convert it to raw uncompressed 64‑byte `X||Y` hex expected by Flow. +1. On the client, generate `PublicKeyCredentialCreationOptions` with: + - `pubKeyCredParams`'s `alg` equal to `ES256` (`-7`) + - the RP id is derived from to the web origin + - the challenge equal to an arbitrary constant +2. On the client, call `navigator.credentials.create()`. +3. Verify attestation if necessary and extract the public key (P‑256 in this guide). Convert it to raw uncompressed 64‑byte `X||Y` hex string as expected by Flow. 4. Submit a transaction to add the key to the Flow account with weight and algorithms: - Signature algorithm: `ECDSA_P256` - Hash algorithm: `SHA2_256` @@ -51,7 +52,7 @@ Libraries like SimpleWebAuthn can parse the COSE key and produce the raw public ### Build creation options and create credential -Minimum example — wallet‑mode registration (challenge can be constant per FLIP): +Minimum example — wallet‑mode registration: This builds `PublicKeyCredentialCreationOptions` for a wallet RP with a constant registration challenge and ES256 (P‑256) so the resulting public key can be registered on a Flow account. @@ -71,8 +72,8 @@ const creationOptions: PublicKeyCredentialCreationOptions = { rp, user, pubKeyCredParams: [ - { type: "public-key", alg: -7 }, // ES256 (P-256 + SHA-256) - // Optionally ES256K if you support secp256k1 Flow keys: + { type: "public-key", alg: -7 }, // ES256 (ECDSA on P-256 with SHA-256) + // Optionally ES256K (ECDSA on secp256k1 with SHA-256) if the device supports secp256k1 keys: // { type: "public-key", alg: -47 }, ], authenticatorSelection: { userVerification: "preferred" }, @@ -82,13 +83,13 @@ const creationOptions: PublicKeyCredentialCreationOptions = { const credential = await navigator.credentials.create({ publicKey: creationOptions }) -// Send to wallet-core (or local) to extract COSE P-256 public key (verify attestation if necessary) +// Send to wallet-core (or local) to extract COSE ECDSA P-256 public key (verify attestation if necessary) // Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 (this guide’s choice) ``` ### Extract and normalize public key -Client-side example — extract COSE public key (no verification) and derive raw uncompressed 64-byte X||Y hex suitable for Flow key registration: +Client-side example — extract COSE ECDSA public key (no verification) and derive raw uncompressed 64-byte `X||Y` hex suitable for Flow key registration: This parses the `attestationObject` to locate the COSE EC2 `credentialPublicKey`, reads the x/y coordinates, and returns raw uncompressed 64-byte `X||Y` hex suitable for Flow key registration. Attestation verification is intentionally omitted here. @@ -124,10 +125,10 @@ function coseEcP256ToUncompressedXYHex(coseKey: Uint8Array): string { const m: Map = CBOR.decode(coseKey) const x = new Uint8Array(m.get(-2)) const y = new Uint8Array(m.get(-3)) - if (x.length !== 32 || y.length !== 32) throw new Error('Invalid P-256 coordinate lengths') + if (x.length > 32 || y.length > 32) throw new Error('Invalid P-256 coordinate lengths') const xy = new Uint8Array(64) - xy.set(x, 0) - xy.set(y, 32) + xy.set(x, 32 - x.length) + xy.set(y, 64 - y.length) return toHex(xy) // 64-byte X||Y hex, no 0x or 0x04 prefix } @@ -143,7 +144,7 @@ const publicKeyHex = coseEcP256ToUncompressedXYHex(cosePubKey) ### Add key to account -Now that you have the user's public key, provision a Flow account with that key. Creating accounts requires payment; in practice, account instantiation typically occurs on the wallet provider's backend service. +Now that you have the user's public key, provision a Flow account with that key. Creating accounts (or adding key to an existing account) requires payment; in practice, account instantiation typically occurs on the wallet provider's backend service. In the PoC demo, we used a test API to provision an account with the public key: @@ -173,14 +174,14 @@ export async function createAccountWithPublicKey( ``` :::note -In production, this would be a service owned by the wallet provider that creates the account and attaches the user's public key, for reasons outlined in [WebAuthn Credential Support (FLIP)] (e.g., payment handling, abuse prevention, telemetry, and correlation as needed). +In production, this would be a service owned by the wallet provider that creates the account and attaches the user's public key, for reasons like payment handling, abuse prevention, telemetry, and correlation as needed. ::: ## Signing ### Generate the challenge -- Assertion (transaction signing): Wallet sets `challenge` to the SHA2‑256 of the signable transaction message (payload or envelope per signer role). No server‑sent challenge is used. Flow includes a domain‑separation tag in the signable bytes. +- Assertion (transaction signing): Wallet sets `challenge` to the SHA2‑256 of the signable transaction message (payload or envelope per signer role). No server‑sent or random challenge is used. Flow includes a domain‑separation tag in the signable bytes. Minimal example — derive signable message and hash (per FLIP): @@ -205,7 +206,7 @@ const msgHex = encodeMessageFromSignable(signable, address) const payloadMsgHex = encodeTransactionPayload(signable.voucher) const role = msgHex === payloadMsgHex ? "payload" : "envelope" -// 2) Compute SHA2-256(msgHex) -> 32-byte challenge (Flow keys commonly use SHA2_256) +// 2) Compute SHA2-256(msgHex) -> 32-byte challenge const signableHash: Uint8Array = sha256(hexToBytes(msgHex)) // 3) Call navigator.credentials.get with challenge = signableHash @@ -213,7 +214,7 @@ const signableHash: Uint8Array = sha256(hexToBytes(msgHex)) ``` :::note -`encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑specific helpers. If you are not using FCL, construct the Flow signable transaction message yourself (payload for proposer/authorizer, envelope for payer), then compute `SHA2‑256(messageBytes)` for the challenge. The payload encoding shown here applies regardless of wallet implementation; the helper calls are simply conveniences from FCL. +`encodeMessageFromSignable` and `encodeTransactionPayload` are FCL‑specific helpers. If you are not using FCL, construct the Flow signable transaction message yourself (payload for proposer/authorizer, envelope for payer, prepended by the transaction domain tag), then compute `SHA2‑256(messageBytes)` for the challenge. The payload encoding shown here applies regardless of wallet implementation; the helper calls are simply conveniences from FCL. ::: ### Request assertion @@ -249,21 +250,21 @@ const { authenticatorData, clientDataJSON, signature } = ``` :::note -Wallets typically know which credential corresponds to the user’s active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn Credential Support (FLIP)] for wallet‑mode guidance. +Wallets typically know which credential corresponds to the user’s active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. ::: ### Convert and attach signature -WebAuthn assertion signatures are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). +WebAuthn assertion signatures in this guide are ECDSA P‑256 over SHA‑256 and are typically returned in ASN.1/DER form. Flow expects raw 64‑byte signatures: `r` and `s` each 32 bytes, concatenated (`r || s`). - Convert the DER `signature` to Flow raw `r||s` (64 bytes) and attach with `addr` and `keyId`. -- Build the signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. +- Build the transaction signature extension as specified: `extension_data = 0x01 || RLP([authenticatorData, clientDataJSON])`. Minimal example — convert and attach for submission: -Convert the DER signature to Flow raw r||s and build `signatureExtension = 0x01 || RLP([authenticatorData, clientDataJSON])` per the FLIP, then compose the Flow signature object for inclusion in your transaction. +Convert the DER signature to Flow raw `r||s` and build `signatureExtension = 0x01 || RLP([authenticatorData, clientDataJSON])` per the FLIP, then compose the Flow transaction signature object for inclusion in your transaction. ```tsx import { encode as rlpEncode } from 'rlp' @@ -351,8 +352,8 @@ function leftPad32(bytes: Uint8Array): Uint8Array { ## Notes from the PoC - The PoC in `fcl-js/packages/passkey-wallet` demonstrates end‑to‑end flows for passkey creation and assertion, including: - - Extracting and normalizing the P‑256 public key for Flow - - Generating secure nonces and verifying account‑proof + - Extracting and normalizing the ECDSA P‑256 public key for Flow + - Building the correct challenge - Converting DER signatures to raw `r||s` - Packaging WebAuthn fields as signature extension data @@ -360,8 +361,7 @@ function leftPad32(bytes: Uint8Array): Uint8Array { ## Security and UX considerations -- Use `ECDSA_P256` with `SHA2_256` for Flow account keys derived from WebAuthn P‑256. -- Enforce nonce expiry, single‑use semantics, and strong server‑side randomness. +- Use `ES256` or `ES256k` as algorithms to create Flow account compatible keys. - Clearly communicate platform prompts and recovery paths; passkeys UX can differ across OS/browsers. - Replay protection: Flow uses on‑chain proposal‑key sequence numbers; see [Replay attacks]. - Optional wallet backend: store short‑lived correlation data or rate‑limits as needed (not required). @@ -384,9 +384,9 @@ In this tutorial, you integrated passkeys (WebAuthn) with Flow for both registra Now that you have completed the tutorial, you should be able to: -- Register a WebAuthn credential and derive a Flow‑compatible public key +- Create a WebAuthn credential and derive a Flow‑compatible public key - Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw r||s format and attach the FLIP signature extension +- Convert a WebAuthn ECDSA DER signature into Flow’s raw `r||s` format and attach the transaction signature extension ### Further reading @@ -413,5 +413,6 @@ Now that you have completed the tutorial, you should be able to: [Signature and Hash Algorithms]: ../../../build/cadence/basics/accounts.md [Flow Client Library]: ../../../build/tools/clients/fcl-js/index.md [Wallet Provider Spec]: ../../../build/tools/wallet-provider-spec/index.md +[WebAuthn specifications]: https://www.w3.org/TR/webauthn-3 From 96749a529d55b6fb24ca57953be2ede111a9b98a Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 10 Oct 2025 07:13:36 -0700 Subject: [PATCH 10/19] update lockfile --- yarn.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index b3b9744dc2..7fff3a1a16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2709,17 +2709,10 @@ resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== -<<<<<<< HEAD -"@fortawesome/fontawesome-common-types@6.7.2": - version "6.7.2" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz" - integrity sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg== -======= "@fortawesome/fontawesome-common-types@7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659" integrity sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA== ->>>>>>> origin/main "@fortawesome/fontawesome-svg-core@^7.1.0": version "7.1.0" From 830b4436311658693832706b6f1ea665dd66dc79 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sun, 12 Oct 2025 10:26:33 -0700 Subject: [PATCH 11/19] Move document & add keywords --- .../forte/index.md | 4 +- .../cadence/advanced-concepts/passkeys.md} | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) rename docs/{blockchain-development-tutorials/forte/passkeys/index.md => build/cadence/advanced-concepts/passkeys.md} (90%) diff --git a/docs/blockchain-development-tutorials/forte/index.md b/docs/blockchain-development-tutorials/forte/index.md index 0220f423ef..2947e1d272 100644 --- a/docs/blockchain-development-tutorials/forte/index.md +++ b/docs/blockchain-development-tutorials/forte/index.md @@ -38,7 +38,7 @@ Discover how to implement scheduled transactions for time-based smart contract e ### [Passkeys (WebAuthn)] -Implement device-backed passkeys using the Web Authentication API to register Flow account keys and sign transactions with secure, user-friendly authentication. +Implement device-backed passkeys using the Web Authentication API to register Flow account keys and sign transactions with secure, user-friendly authentication. See the [advanced concepts documentation](../../build/cadence/advanced-concepts/passkeys.md) for detailed implementation guidance. ## Getting Started @@ -62,6 +62,6 @@ The Forte network upgrade represents a significant evolution of Flow's capabilit [Flow Actions]: ./flow-actions/index.md [Scheduled Transactions]: ./scheduled-transactions/index.md -[Passkeys (WebAuthn)]: ./passkeys/index.md +[Passkeys (WebAuthn)]: ../../build/cadence/advanced-concepts/passkeys.md [Introduction to Flow Actions]: ./flow-actions/intro-to-flow-actions.md [Scheduled Transactions Introduction]: ./scheduled-transactions/scheduled-transactions-introduction.md diff --git a/docs/blockchain-development-tutorials/forte/passkeys/index.md b/docs/build/cadence/advanced-concepts/passkeys.md similarity index 90% rename from docs/blockchain-development-tutorials/forte/passkeys/index.md rename to docs/build/cadence/advanced-concepts/passkeys.md index 13b3ea79e5..21ac5ea57e 100644 --- a/docs/blockchain-development-tutorials/forte/passkeys/index.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -1,9 +1,23 @@ --- title: Passkeys description: Implement passkeys on Flow using WebAuthn, covering key extraction, challenges, signature formatting for Flow, and signature extensions. -sidebar_position: 5 keywords: - passkeys + - WebAuthn + - authentication + - ECDSA P256 + - ES256 + - Flow account keys + - wallet integration + - credential management + - signature verification + - biometric authentication + - FIDO2 + - multi-factor authentication + - passwordless authentication + - Flow transactions + - public key cryptography +sidebar_position: 9 --- # Passkeys @@ -11,17 +25,17 @@ keywords: This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) that covers end‑to‑end passkeys integration for Flow: 1. Create a passkey and add a Flow account key -2. Sign a transaction with the user’s passkey (includes conversion, extension, and submission) +2. Sign a transaction with the user's passkey (includes conversion, extension, and submission) It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. -## What you’ll learn +## What you'll learn After completing this guide, you'll be able to: - Create a passkey and derive a Flow‑compatible public key - Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw `r||s` format and attach the transaction signature extension +- Convert a WebAuthn ECDSA DER signature into Flow's raw `r||s` format and attach the transaction signature extension ## Prerequisites @@ -32,7 +46,7 @@ After completing this guide, you'll be able to: ## Registration -When a user generates a passkey via [navigator.credentials.create()] with `{ publicKey }`, the authenticator returns an attestation containing the new credential’s public key. On Flow, you can register that public key on an account if the algorithm of the requested passkey is either `ES256` or `ES256k`. This guide demonstrates an `ES256` passkey which translates to an `ECDSA_P256` Flow key paired with `SHA2_256` hashing. Althernatively, an `ES256k` passkey translates to an `ECDSA_secp256k1` Flow key paired with `SHA2_256` hashing. +When a user generates a passkey via [navigator.credentials.create()] with `{ publicKey }`, the authenticator returns an attestation containing the new credential's public key. On Flow, you can register that public key on an account if the algorithm of the requested passkey is either `ES256` or `ES256k`. This guide demonstrates an `ES256` passkey which translates to an `ECDSA_P256` Flow key paired with `SHA2_256` hashing. Alternatively, an `ES256k` passkey translates to an `ECDSA_secp256k1` Flow key paired with `SHA2_256` hashing. High‑level steps: @@ -84,7 +98,7 @@ const creationOptions: PublicKeyCredentialCreationOptions = { const credential = await navigator.credentials.create({ publicKey: creationOptions }) // Send to wallet-core (or local) to extract COSE ECDSA P-256 public key (verify attestation if necessary) -// Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 (this guide’s choice) +// Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 (this guide's choice) ``` ### Extract and normalize public key @@ -250,7 +264,7 @@ const { authenticatorData, clientDataJSON, signature } = ``` :::note -Wallets typically know which credential corresponds to the user’s active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. +Wallets typically know which credential corresponds to the user's active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. ::: @@ -386,7 +400,7 @@ Now that you have completed the tutorial, you should be able to: - Create a WebAuthn credential and derive a Flow‑compatible public key - Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) -- Convert a WebAuthn ECDSA DER signature into Flow’s raw `r||s` format and attach the transaction signature extension +- Convert a WebAuthn ECDSA DER signature into Flow's raw `r||s` format and attach the transaction signature extension ### Further reading @@ -409,10 +423,11 @@ Now that you have completed the tutorial, you should be able to: [AuthenticatorAttestationResponse]: https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse [AuthenticatorAssertionResponse]: https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse [Replay attacks]: https://github.com/onflow/flips/blob/cfaaf5f6b7c752e8db770e61ec9c180dc0eb6543/protocol/20250203-webauthn-credential-support.md#replay-attacks -[Transactions]: ../../../build/cadence/basics/transactions.md -[Signature and Hash Algorithms]: ../../../build/cadence/basics/accounts.md -[Flow Client Library]: ../../../build/tools/clients/fcl-js/index.md -[Wallet Provider Spec]: ../../../build/tools/wallet-provider-spec/index.md +[Transactions]: ../basics/transactions.md +[Signature and Hash Algorithms]: ../basics/accounts.md +[Flow Client Library]: ../../tools/clients/fcl-js/index.md +[Wallet Provider Spec]: ../../tools/wallet-provider-spec/index.md [WebAuthn specifications]: https://www.w3.org/TR/webauthn-3 + From e1d6e59f8de7db74bf4608743030d687f917099b Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sun, 12 Oct 2025 10:31:44 -0700 Subject: [PATCH 12/19] Add benefits/limitations --- .../cadence/advanced-concepts/passkeys.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index 21ac5ea57e..dfa20083d9 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -37,6 +37,26 @@ After completing this guide, you'll be able to: - Generate the correct challenge for signing transactions (wallet sets SHA2‑256(signable)) - Convert a WebAuthn ECDSA DER signature into Flow's raw `r||s` format and attach the transaction signature extension +## Benefits of using passkeys + +**Sign transactions securely** +Users can sign Flow transactions using passkeys while the private key stays securely stored within the authenticator. This reduces the risk of key extraction attacks and phishing attempts. + +**Authenticate across devices** +Users can scan a QR code displayed on a desktop browser with a mobile device to approve transactions. Cloud-synchronized passkeys (such as those stored in Apple iCloud or Google Password Manager) enable authentication across multiple devices without manual key transfers. + +**Use hardware security keys** +Users can sign transactions with external security keys, such as YubiKeys, to add another layer of protection against phishing and unauthorized access. + +**Authenticate with platform-based security** +Users can sign transactions directly on devices with built-in authenticators, such as Face ID on iPhones or Windows Hello on Windows PCs. This approach enables native transaction signing without needing an external security key. + +**Recover access with cloud-synced passkeys** +Cloud-synced passkeys help users recover access if they lose a device, though this introduces trade-offs between convenience and self-custody (see [Limitations of passkeys](#limitations-of-passkeys)). + +**Work with multi-key accounts** +Combine passkeys with other authentication types using Flow's native [multi-key account support](../basics/accounts.md#account-keys) to build secure recovery options and shared access patterns with weighted keys. + ## Prerequisites - Working knowledge of modern frontend (React/Next.js) and basic backend @@ -380,6 +400,17 @@ function leftPad32(bytes: Uint8Array): Uint8Array { - Replay protection: Flow uses on‑chain proposal‑key sequence numbers; see [Replay attacks]. - Optional wallet backend: store short‑lived correlation data or rate‑limits as needed (not required). +## Limitations of passkeys + +**Functionality varies by authenticator** +Some security keys do not support biometric authentication, requiring users to enter a PIN instead. Because WebAuthn does not provide access to private keys, users must either store their passkey securely or enable cloud synchronization for recovery. + +**Cloud synchronization introduces risks** +Cloud-synced passkeys improve accessibility but also create risks if a cloud provider is compromised or if a user loses access to their cloud account. Users who prefer full self-custody can use hardware-based passkeys that do not rely on cloud synchronization. + +**Passkeys cannot be exported** +Users cannot transfer a passkey between different authenticators. For example, a passkey created on a security key cannot move to another device unless it syncs through a cloud provider. To avoid losing access, users should set up authentication on multiple devices or combine passkeys with [multi-key account configurations](../basics/accounts.md#account-keys) for additional recovery options. + ## Credential management (wallet responsibilities) From 71f67d058277280cc5140f2d0f44f07b180b85de Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sun, 12 Oct 2025 10:34:58 -0700 Subject: [PATCH 13/19] Add note for other platforms --- docs/build/cadence/advanced-concepts/passkeys.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index dfa20083d9..24df85ab30 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -29,6 +29,10 @@ This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) t It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. +:::note Platform-specific APIs +This tutorial focuses on the **Web Authentication API** (WebAuthn) for browser-based applications. Other platforms such as iOS, Android, and desktop applications will require platform-specific APIs (e.g., Apple's [Authentication Services](https://developer.apple.com/documentation/authenticationservices), Android's [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)), but the underlying concepts—credential creation, challenge-response signing, and signature formatting—remain the same across all platforms. +::: + ## What you'll learn After completing this guide, you'll be able to: @@ -121,6 +125,15 @@ const credential = await navigator.credentials.create({ publicKey: creationOptio // Then register the raw uncompressed key bytes on the Flow account as ECDSA_P256/SHA2_256 (this guide's choice) ``` +:::tip RP ID for non-browser platforms +For web applications, `rpId` is set to `window.location.hostname`. For native mobile and desktop applications, use your app's identifier instead: +- **iOS**: Use your app's bundle identifier (e.g., `com.example.wallet`) or an associated domain +- **Android**: Use your app's package name (e.g., `com.example.wallet`) or an associated domain +- **Desktop**: Use your application identifier or registered domain + +The rpId must remain consistent across credential creation and assertion for the same user account. +::: + ### Extract and normalize public key Client-side example — extract COSE ECDSA public key (no verification) and derive raw uncompressed 64-byte `X||Y` hex suitable for Flow key registration: @@ -284,7 +297,8 @@ const { authenticatorData, clientDataJSON, signature } = ``` :::note -Wallets typically know which credential corresponds to the user's active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. +- **Credential selection**: Wallets typically know which credential corresponds to the user's active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. +- **RP ID consistency**: The `rpId` used here must match exactly what was used during credential creation. For non-browser platforms, use the same app identifier (bundle ID, package name, etc.) as in registration. ::: From f447fa6f42ff5ec06386b7cf1a427392d9d50c98 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sun, 12 Oct 2025 10:37:37 -0700 Subject: [PATCH 14/19] remove webauthn --- docs/blockchain-development-tutorials/forte/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blockchain-development-tutorials/forte/index.md b/docs/blockchain-development-tutorials/forte/index.md index 2947e1d272..3582c023ad 100644 --- a/docs/blockchain-development-tutorials/forte/index.md +++ b/docs/blockchain-development-tutorials/forte/index.md @@ -36,7 +36,7 @@ Learn how to build decentralized finance applications using the Flow Actions fra Discover how to implement scheduled transactions for time-based smart contract execution on Flow. These tutorials cover creating automated workflows, cron-like functionality, and time-sensitive blockchain applications that can execute without manual intervention. -### [Passkeys (WebAuthn)] +### [Passkeys] Implement device-backed passkeys using the Web Authentication API to register Flow account keys and sign transactions with secure, user-friendly authentication. See the [advanced concepts documentation](../../build/cadence/advanced-concepts/passkeys.md) for detailed implementation guidance. @@ -62,6 +62,6 @@ The Forte network upgrade represents a significant evolution of Flow's capabilit [Flow Actions]: ./flow-actions/index.md [Scheduled Transactions]: ./scheduled-transactions/index.md -[Passkeys (WebAuthn)]: ../../build/cadence/advanced-concepts/passkeys.md +[Passkeys]: ../../build/cadence/advanced-concepts/passkeys.md [Introduction to Flow Actions]: ./flow-actions/intro-to-flow-actions.md [Scheduled Transactions Introduction]: ./scheduled-transactions/scheduled-transactions-introduction.md From 4c39131c2052698f571d4760291d9426168f88ed Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sun, 12 Oct 2025 15:11:16 -0700 Subject: [PATCH 15/19] Add updated PoC --- docs/build/cadence/advanced-concepts/passkeys.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index 24df85ab30..d170db2551 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -27,7 +27,7 @@ This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) t 1. Create a passkey and add a Flow account key 2. Sign a transaction with the user's passkey (includes conversion, extension, and submission) -It accompanies the PoC in `fcl-js/packages/passkey-wallet` for reference and cites the FLIP where behavior is normative. +It accompanies the [PoC demo](https://github.com/onflow/passkey-wallet-demo) for reference and cites the FLIP where behavior is normative. :::note Platform-specific APIs This tutorial focuses on the **Web Authentication API** (WebAuthn) for browser-based applications. Other platforms such as iOS, Android, and desktop applications will require platform-specific APIs (e.g., Apple's [Authentication Services](https://developer.apple.com/documentation/authenticationservices), Android's [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)), but the underlying concepts—credential creation, challenge-response signing, and signature formatting—remain the same across all platforms. @@ -399,7 +399,7 @@ function leftPad32(bytes: Uint8Array): Uint8Array { ## Notes from the PoC -- The PoC in `fcl-js/packages/passkey-wallet` demonstrates end‑to‑end flows for passkey creation and assertion, including: +- The [PoC demo](https://github.com/onflow/passkey-wallet-demo) demonstrates end‑to‑end flows for passkey creation and assertion, including: - Extracting and normalizing the ECDSA P‑256 public key for Flow - Building the correct challenge - Converting DER signatures to raw `r||s` From 160e74d588d08f7d9193f7cd5f6007ae45df2518 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:35:00 -0700 Subject: [PATCH 16/19] Update docs/build/cadence/advanced-concepts/passkeys.md Co-authored-by: Tarak Ben Youssef <50252200+tarakby@users.noreply.github.com> --- docs/build/cadence/advanced-concepts/passkeys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index d170db2551..7016fa2947 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -30,7 +30,7 @@ This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) t It accompanies the [PoC demo](https://github.com/onflow/passkey-wallet-demo) for reference and cites the FLIP where behavior is normative. :::note Platform-specific APIs -This tutorial focuses on the **Web Authentication API** (WebAuthn) for browser-based applications. Other platforms such as iOS, Android, and desktop applications will require platform-specific APIs (e.g., Apple's [Authentication Services](https://developer.apple.com/documentation/authenticationservices), Android's [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)), but the underlying concepts—credential creation, challenge-response signing, and signature formatting—remain the same across all platforms. +This tutorial focuses on the **Web Authentication API** (WebAuthn) for browser-based applications. Other platforms such as iOS, Android, and desktop applications will require platform-specific APIs (e.g., Apple's [Authentication Services](https://developer.apple.com/documentation/authenticationservices), Android's [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)), but the underlying concepts—credential creation, challenge signing, and signature formatting—remain the same across all platforms. ::: ## What you'll learn From a9e33551be2b6093ed293496080623f08f35b16a Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 14 Oct 2025 15:01:19 -0700 Subject: [PATCH 17/19] address feedback --- docs/build/cadence/advanced-concepts/passkeys.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index 7016fa2947..bb6c8beb41 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -48,10 +48,6 @@ Users can sign Flow transactions using passkeys while the private key stays secu **Authenticate across devices** Users can scan a QR code displayed on a desktop browser with a mobile device to approve transactions. Cloud-synchronized passkeys (such as those stored in Apple iCloud or Google Password Manager) enable authentication across multiple devices without manual key transfers. - -**Use hardware security keys** -Users can sign transactions with external security keys, such as YubiKeys, to add another layer of protection against phishing and unauthorized access. - **Authenticate with platform-based security** Users can sign transactions directly on devices with built-in authenticators, such as Face ID on iPhones or Windows Hello on Windows PCs. This approach enables native transaction signing without needing an external security key. @@ -131,7 +127,7 @@ For web applications, `rpId` is set to `window.location.hostname`. For native mo - **Android**: Use your app's package name (e.g., `com.example.wallet`) or an associated domain - **Desktop**: Use your application identifier or registered domain -The rpId must remain consistent across credential creation and assertion for the same user account. +The rpId should remain consistent across credential creation and assertion for the same user account; however, this consistency is not validated or enforced by Flow. ::: ### Extract and normalize public key @@ -298,7 +294,7 @@ const { authenticatorData, clientDataJSON, signature } = :::note - **Credential selection**: Wallets typically know which credential corresponds to the user's active account (selected during authentication/authorization), so they should pass that credential via `allowCredentials` to scope selection and minimize prompts. For discoverable credentials, omitting `allowCredentials` is also valid and lets the authenticator surface available credentials. See [WebAuthn specifications] for guidance. -- **RP ID consistency**: The `rpId` used here must match exactly what was used during credential creation. For non-browser platforms, use the same app identifier (bundle ID, package name, etc.) as in registration. +- **RP ID consistency**: The `rpId` used here should match what was used during credential creation; however, Flow does not validate or enforce this (transactions would still pass even if different). For non-browser platforms, use the same app identifier (bundle ID, package name, etc.) as in registration. ::: From 842421dcce1f0034f7f00d7d09d8d72810f73997 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 14 Oct 2025 15:02:30 -0700 Subject: [PATCH 18/19] fix spacing --- docs/build/cadence/advanced-concepts/passkeys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index bb6c8beb41..4efc7a36d9 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -48,6 +48,7 @@ Users can sign Flow transactions using passkeys while the private key stays secu **Authenticate across devices** Users can scan a QR code displayed on a desktop browser with a mobile device to approve transactions. Cloud-synchronized passkeys (such as those stored in Apple iCloud or Google Password Manager) enable authentication across multiple devices without manual key transfers. + **Authenticate with platform-based security** Users can sign transactions directly on devices with built-in authenticators, such as Face ID on iPhones or Windows Hello on Windows PCs. This approach enables native transaction signing without needing an external security key. From 462f933f4b6331edecbf1c6e73d9d5e6f9f0eabd Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 14 Oct 2025 15:11:48 -0700 Subject: [PATCH 19/19] adjust framing --- docs/build/cadence/advanced-concepts/passkeys.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/build/cadence/advanced-concepts/passkeys.md b/docs/build/cadence/advanced-concepts/passkeys.md index 4efc7a36d9..34aed13542 100644 --- a/docs/build/cadence/advanced-concepts/passkeys.md +++ b/docs/build/cadence/advanced-concepts/passkeys.md @@ -22,7 +22,7 @@ sidebar_position: 9 # Passkeys -This is a wallet‑centric guide (per [FLIP 264: WebAuthn Credential Support]) that covers end‑to‑end passkeys integration for Flow: +This is a wallet‑centric, high‑level guide (per [FLIP 264: WebAuthn Credential Support]) with code snippets covering passkey registration and signing on Flow, focusing on nuances for passkey signing and account keys: 1. Create a passkey and add a Flow account key 2. Sign a transaction with the user's passkey (includes conversion, extension, and submission) @@ -396,7 +396,7 @@ function leftPad32(bytes: Uint8Array): Uint8Array { ## Notes from the PoC -- The [PoC demo](https://github.com/onflow/passkey-wallet-demo) demonstrates end‑to‑end flows for passkey creation and assertion, including: +- The [PoC demo](https://github.com/onflow/passkey-wallet-demo) demonstrates reference flows for passkey creation and assertion, including: - Extracting and normalizing the ECDSA P‑256 public key for Flow - Building the correct challenge - Converting DER signatures to raw `r||s`