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:- Register — create a WebAuthn credential, extract its COSE public key, and register it as a
key_type: WEBAUTHNsigner. - 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.
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 ispubKeyCredParams (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
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.
2. Sign and submit a transaction
The intent andoperation 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
What the verifier does, step by step: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 request —intent and signatures at the top level. There is no wrapper object.
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:
| Field | Requirement |
|---|---|
id | Required. Base64url (no padding) — this is validated. |
type | Required. Must be "public-key". |
rawId | Recommended (standard toJSON() output). |
response.authenticatorData | Required. Base64url of the raw authenticator data. |
response.clientDataJSON | Required. Base64url; decodes to JSON containing type: "webauthn.get" and the challenge. |
response.signature | Required. Base64url; DER for ES256, PKCS#1 v1.5 for RS256. |
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-validaterpId 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
| Symptom | Likely cause | Fix |
|---|---|---|
WebAuthn challenge hash does not match intentHash | Challenge was set to SHA-256(intent) instead of the raw canonical JSON bytes; or the intent was mutated between signing and submission | Use the canonical bytes directly as the challenge, and submit the same intent object you signed. |
failed to parse WebAuthn response | A response field (or id) is not base64url, or authenticatorData / clientDataJSON is malformed | Use the standard assertion.toJSON() output; do not re-encode its inner fields. id must be base64url with no padding. |
failed to unmarshal WebAuthn signature | The decoded signatures[] entry is not the assertion JSON | Ensure each entry is base64( JSON(assertion) ), not the raw assertion object and not just the signature field. |
failed to parse WebAuthn public key | The registered public_key is a PKIX/PEM key, or the COSE algorithm is unsupported | Register the raw COSE key (1b), and use an ES256 or RS256 credential. |
signature verification failed | allowCredentials referenced a credential whose key does not match the registered signer | Confirm the credential.id you sign with corresponds to the COSE key you registered. |
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.
