Skip to main content
Dakota signers can be WebAuthn credentials (passkeys, platform authenticators, or roaming keys like a YubiKey) in addition to raw ES256 keys. A WebAuthn signer endorses an intent by producing a standard WebAuthn assertion — the same object a browser returns from navigator.credentials.get() — rather than a bare ECDSA signature over the intent hash. This page is the reference for the WebAuthn path. If you sign with raw ES256 keys, read Wallet Transaction Signing instead. For the conceptual model, intent catalogue, and signer-group / policy lifecycles (which are identical for both signer types), read Signing & Endorsed Requests.
Why WebAuthn is different. An authenticator never signs an arbitrary hash. It signs authenticatorData ‖ SHA-256(clientDataJSON), and the intent reaches the signature only as the challenge embedded inside clientDataJSON. The verifier therefore needs the whole assertion bundle (authenticatorData, clientDataJSON, signature) — not a DER signature. Everything below exists to thread the intent through that bundle correctly.

The model at a glance

A WebAuthn integration has two phases:
  1. Register — create a WebAuthn credential, extract its COSE public key, and register it as a key_type: WEBAUTHN signer.
  2. Sign — build a transaction intent, JCS-canonicalize it, use those exact bytes as the WebAuthn challenge, produce an assertion, and submit it in an endorsed request.
Once a WebAuthn signer is in a signer group attached to a wallet, it participates in approval thresholds exactly like an ES256 signer. The signer-group and policy mechanics in Signing & Endorsed Requests apply unchanged.
Supported algorithms. A WEBAUTHN signer’s COSE key must be ES256 (EC2 / P-256, COSE alg -7) or RS256 (RSA, 2048-bit minimum, COSE alg -257). Other algorithms and curves — including ES384/ES512, Ed25519, and secp256k1 — are rejected at verification time. Virtually all passkeys are ES256, so request [-7, -257] and you will get an ES256 credential.

1. Register a WEBAUTHN signer

1a. Create the credential (browser)

Run a normal WebAuthn registration ceremony. The relying-party options come from your own server; the only Dakota-specific constraint is pubKeyCredParams (i.e. supportedAlgorithmIDs) limited to ES256 / RS256.

1b. Extract the COSE public key (server)

The public key Dakota stores is the raw COSE key (CBOR) embedded in the attestation object — not a PKIX/SPKI blob. The simplest way to get it is to verify the registration with @simplewebauthn/server and read the credential public key it returns:
If you are not using @simplewebauthn/server, the COSE key is the credentialPublicKey field of attestedCredentialData inside the authData of the attestationObject. Decode the attestation CBOR with any CBOR library and walk the standard authenticator-data layout to extract it. The bytes you register are exactly that COSE key, base64-encoded.

1c. Register the signer

Save both the returned signer id (for building signer groups) and the credential.id from step 1a (for signing). Add the signer to a signer group and attach that group to a wallet exactly as you would for an ES256 signer — see the Signer Group Lifecycle.
public_key for a WEBAUTHN signer is the raw CBOR COSE key, base64-encoded. This is a different encoding from key_type: ES256, which expects an X.509 SubjectPublicKeyInfo (PKIX) DER/PEM blob. Sending a PKIX key for a WebAuthn signer (or vice-versa) fails verification later, not at registration.

2. Sign and submit a transaction

The intent and operation schema are identical to the ECDSA flow — only the signing step changes.

2a. Build and canonicalize the intent

2b. The challenge — the one thing that trips integrators up

The challenge is the raw bytes of the RFC 8785 (JCS) canonical intent JSONnot SHA-256(intent), and not any other digest. Do not hash the intent before using it as the challenge. The verifier re-derives the hash itself.
What the verifier does, step by step:
Because the verifier re-canonicalizes whatever it decodes from the challenge, minor formatting differences (key order, trailing decimal zeros, empty strings) are tolerated. Signing the output of your JCS library directly is the safest approach.
context_digest participates automatically. It is an optional field on SendTransactionIntent; because it lives inside the intent object, it is part of the canonical JSON the challenge commits to — no separate handling. The hash is a single SHA-256, not a double hash.

2c. Produce the assertion (browser)

2d. Submit the endorsed request

The request body is the endorsed requestintent and signatures at the top level. There is no wrapper object.
The intent you submit must be the same object you canonicalized and signed — the verifier independently canonicalizes it and compares its hash against the challenge. The signatures array holds one entry per endorsing signer, up to the signer group’s approval threshold.

End-to-end


What goes in signatures[]

Each entry is the WebAuthn assertion serialized as JSON, then base64-encoded — there is no CBOR, no custom concatenation, and no proprietary envelope:
The decoded JSON:
JSON field order is irrelevant. The fields inside response must be base64url; the outer signatures[] string itself may be standard base64 or base64url, padded or not — the decoder accepts all four.

rpId and origin

Dakota does not re-validate rpId or origin server-side. The verifier extracts authenticatorData, clientDataJSON, and signature, checks the challenge binding, and verifies the signature against the registered COSE key. The rpId and origin are cryptographically bound into the signature but are not compared against an expected value. You still need a valid, stable rpId configured in your relying party to complete the browser ceremony in the first place — use your real domain (and keep it consistent between registration and signing). clientDataJSON.type will be "webauthn.get" naturally.

Troubleshooting

Multi-signer approvals. When a wallet’s policy requires more than one approval, collect one signatures[] entry per signer (each a base64url assertion JSON) and submit them together. WebAuthn and ES256 signatures can be mixed in the same signatures array.