LayerZeroFault
passkey recovery

WebCrypto P-256 SPKI DER Header Mismatch in Passkey Smart Accounts

VV

Written by

Published:

Technical Guide

To resolve WebCrypto P-256 SPKI DER Header Mismatch in Passkey Smart Accounts, resolve WebCrypto DOMException: Data provided does not meet requirements and on-chain P-256 precompile reverts when converting SPKI DER passkey keys to raw coordinates. Ensure all derivation paths, environment parameters, and cryptographic signatures are verified client-side using deterministic tools before broadcasting live transactions.

In modern decentralized applications leveraging ERC-4337 Account Abstraction and Passkey Embedded Signers (such as Turnkey, Privy, Biconomy, or ZeroDev), cryptographic keys are generated using the NIST secp256r1 (P-256) elliptic curve via the browser’s native WebAuthn API.

However, frontend engineers and smart contract developers frequently hit fatal runtime exceptions when bridging public keys between the Browser WebCrypto API (window.crypto.subtle), backend indexing nodes, and on-chain P-256 verifiers:

DOMException: Data provided to an operation does not meet requirements
  name: "DataError"
  code: 0
  at window.crypto.subtle.importKey (webcrypto-signer.ts:84)
  at verifyPasskeyRegistration (turnkey-bridge.ts:219)

And in on-chain transaction simulations:

RPC Error: -32500: EntryPoint simulation failed: AA23 reverted
  Reverted with custom error: InvalidPublicKeyEncoding(expected: 64 bytes, received: 91 bytes)
  Contract: 0x0000000000000000000000000000000000000100 (RIP-7212 P256 Precompile)
  Account: 0x8a15De32810C73d328F421C00742d46e395F7F0e

The underlying issue is a fundamental representation mismatch: WebCrypto demands a 91-byte ASN.1 DER SPKI structure, whereas EVM smart contracts demand raw 64-byte affine coordinates $(X, Y)$.

1. Cryptographic Structure Decomposition

A NIST P-256 public key exists in three distinct formats across the Web3 stack:

Format A: Raw Affine Coordinates (EVM Precompiles & RIP-7212)

  • Length: Exactly 64 bytes.
  • Composition: $X$ coordinate (32 bytes) $\parallel$ $Y$ coordinate (32 bytes).
  • Used by: RIP-7212 (0x100), Daimo P256.sol, FreshCryptoLib, and smart wallet storage.

Format B: ANSI X9.62 Uncompressed Point

  • Length: Exactly 65 bytes.
  • Composition: 0x04 prefix $\parallel$ $X$ coordinate (32 bytes) $\parallel$ $Y$ coordinate (32 bytes).
  • Used by: Elliptic curve libraries and COSE key decoders.

Format C: SubjectPublicKeyInfo (SPKI) DER

  • Length: Exactly 91 bytes.
  • Composition: 26-byte ASN.1 metadata prefix $\parallel$ 65-byte ANSI X9.62 point.
  • Used by: crypto.subtle.importKey("spki", ...) and OpenSSL.

The Canonical 26-Byte DER Header

30 59                               ; SEQUENCE (89 bytes)
   30 13                            ; SEQUENCE (19 bytes)
      06 07 2a 86 48 ce 3d 02 01    ; OID: 1.2.840.10045.2.1 (id-ecPublicKey)
      06 08 2a 86 48 ce 3d 03 01 07 ; OID: 1.2.840.10045.3.1.7 (secp256r1 / P-256)
   03 42 00                         ; BIT STRING (66 bytes, 0 unused bits)
      04 [64 bytes of X and Y]      ; Uncompressed Point

If an application attempts to pass Format A (64 bytes) to crypto.subtle.importKey("spki"), the parser expects the 0x30 sequence header and immediately crashes with DataError.

2. Universal Conversion Utilities (TypeScript)

To ensure seamless interoperability across WebCrypto, Viem, and on-chain contracts, deploy these bidirectional conversion functions:

// utils/p256Encoding.ts

const P256_SPKI_PREFIX = new Uint8Array([
  0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
  0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00,
]);

/**
 * Converts a 91-byte WebAuthn SPKI DER ArrayBuffer into raw 64-byte EVM coordinates.
 */
export function spkiDerToRawCoords(spkiBuffer: ArrayBuffer | Uint8Array): {
  x: `0x${string}`;
  y: `0x${string}`;
  raw64: `0x${string}`;
} {
  const bytes = new Uint8Array(spkiBuffer);

  if (bytes.length !== 91) {
    throw new Error(`Invalid SPKI DER length. Expected 91 bytes, received ${bytes.length}`);
  }

  // Verify the uncompressed point indicator at byte offset 26
  if (bytes[26] !== 0x04) {
    throw new Error(`Expected uncompressed point indicator (0x04) at offset 26, received 0x${bytes[26].toString(16)}`);
  }

  // Extract 32-byte X and Y
  const xBytes = bytes.slice(27, 59);
  const yBytes = bytes.slice(59, 91);

  const toHex = (buf: Uint8Array) =>
    '0x' + Array.from(buf).map((b) => b.toString(16).padStart(2, '0')).join('');

  return {
    x: toHex(xBytes) as `0x${string}`,
    y: toHex(yBytes) as `0x${string}`,
    raw64: toHex(bytes.slice(27, 91)) as `0x${string}`,
  };
}

/**
 * Wraps raw 64-byte (X, Y) coordinates into a 91-byte SPKI ArrayBuffer for WebCrypto.
 */
export function rawCoordsToSpki(xHex: string, yHex: string): ArrayBuffer {
  const cleanX = xHex.replace(/^0x/, '').padStart(64, '0');
  const cleanY = yHex.replace(/^0x/, '').padStart(64, '0');

  const xBytes = new Uint8Array(cleanX.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
  const yBytes = new Uint8Array(cleanY.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));

  const spki = new Uint8Array(91);
  spki.set(P256_SPKI_PREFIX, 0);
  spki[26] = 0x04; // Uncompressed point marker
  spki.set(xBytes, 27);
  spki.set(yBytes, 59);

  return spki.buffer;
}

3. WebCrypto Integration Example

When verifying or re-importing a passkey public key stored on-chain:

import { rawCoordsToSpki } from './utils/p256Encoding';

export async function importOnChainPasskeyToWebCrypto(xCoord: string, yCoord: string): Promise<CryptoKey> {
  // 1. Wrap the on-chain coordinates in the 26-byte SPKI header
  const spkiBuffer = rawCoordsToSpki(xCoord, yCoord);

  // 2. Safely import into WebCrypto without DataError
  const cryptoKey = await window.crypto.subtle.importKey(
    'spki',
    spkiBuffer,
    {
      name: 'ECDSA',
      namedCurve: 'P-256',
    },
    true,
    ['verify']
  );

  return cryptoKey;
}

4. Smart Contract Registration Example (Foundry & Viem)

When registering a passkey signer to a smart account via Viem:

import { spkiDerToRawCoords } from './utils/p256Encoding';

// 1. Retrieve passkey credential from browser
const credential = (await navigator.credentials.create(authOptions)) as PublicKeyCredential;
const spkiDer = (credential.response as AuthenticatorAttestationResponse).getPublicKey();

if (!spkiDer) {
  throw new Error('Authenticator did not return SPKI public key buffer.');
}

// 2. Convert to raw 32-byte words for on-chain storage
const { x, y } = spkiDerToRawCoords(spkiDer);

// 3. Send transaction to configure PasskeyValidator module
await walletClient.writeContract({
  address: SMART_ACCOUNT_VALIDATOR,
  abi: validatorAbi,
  functionName: 'addPasskeySigner',
  args: [BigInt(x), BigInt(y)],
});

5. Summary Verification Checklist

  • Never pass raw 64-byte coordinates directly to crypto.subtle.importKey("spki", ...).
  • Always strip the 27-byte prefix (26 bytes header + 0x04 marker) before storing public keys in Solidity contracts.
  • Ensure $X$ and $Y$ coordinates are zero-padded to exactly 32 bytes each to prevent byte alignment mismatches.
  • Verify whether your target EVM chain supports the native RIP-7212 precompile (0x100) or relies on an EVM fallback verifier.

[!TIP] Diagnostic Verification & Air-Gapped Testing: Before executing state changes or broad recovery steps, cross-reference failure parameters with our companion guide on Windows Hello Passkeys in TPM 2.0: Non-Exportable Private Key & Enclave Lock Fix and verify transaction calldata or cryptographic envelopes using the WebAuthn & Passkey Enclave Diagnostics Debugger.

Interactive Diagnostic Sandbox

Local WebAuthn & Smart Account Sandboxes

Debug WebAuthn credential requests, PRF extensions, and simulate ERC-4337 passkey account validation in client WebCrypto.

Companion Diagnostic Guide Android Credential Manager WebAuthn NotAllowedError & Lock Fix →

Fix Android Credential Manager NotAllowedError and 'Authentication needed: Enter password or unlock'. Resolve biometric enclaves and assetlinks.json.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why does window.crypto.subtle.importKey('spki', ...) throw 'Data provided to an operation does not meet requirements'?

The WebCrypto API's SPKI format expects a strict 91-byte ASN.1 DER SubjectPublicKeyInfo payload containing the ECDSA secp256r1 algorithm identifier (1.2.840.10045.2.1) and named curve OID (1.2.840.10045.3.1.7). If developers pass raw 64-byte (X, Y) coordinates or an uncompressed 65-byte ANSI X9.62 point (0x04 || X || Y) without the 26-byte ASN.1 prefix header, the browser's cryptographic parser immediately rejects the key.

Why do smart contract passkey validators revert when given a WebAuthn public key?

On-chain WebAuthn verifiers (such as RIP-7212 precompiles at 0x100 or Solidity secp256r1 libraries) require exactly two 32-byte words representing the raw affine coordinates (uint256 x, uint256 y). When developers directly forward the 91-byte ArrayBuffer returned by credential.response.getPublicKey(), the smart contract reverts with 'InvalidPublicKeyLength(91)' or fails signature verification.

What is the exact 26-byte ASN.1 DER header for P-256 (secp256r1) public keys?

The canonical 26-byte DER prefix in hexadecimal is: 3059301306072a8648ce3d020106082a8648ce3d030107034200. Appending this prefix to an uncompressed 65-byte public key (starting with 0x04) yields the 91-byte SPKI structure required by WebCrypto and Node.js crypto.subtle.