Fix: ERC-4337 Passkey P256 Signature Revert (AA23 User Verification Mismatch)
During the deployment and testing of ERC-4337 smart accounts utilizing native Passkey authentication (via WebAuthn and secp256r1 / P-256 curves), developers often encounter a fatal bundler simulation failure:
RPC Error: -32500: EntryPoint simulation failed: AA23 reverted (or empty return data)
at EntryPoint.simulateValidation(UserOperation)
at SmartAccount.validateUserOp(0x832a...912f)
The UserOperation simulates locally or passes client-side schema validation, yet bundlers (such as Pimlico, Biconomy, Alchemy, or ZeroDev) instantly drop the transaction with AA23 reverted.
The root cause of this failure mode stems from either a WebAuthn authenticatorData flag bitmask mismatch (specifically regarding the User Verification bit) or an unnormalized high-$s$ ECDSA scalar rejected by strict on-chain RIP-7212 precompiles or modular verifier contracts.
If you are diagnosing gas estimation reverts at the paymaster stage instead of account signature validation, consult our guide on ERC-4337 UserOperation Reverts at Paymaster: PostOp & Validation Failures.
Architectural Deep-Dive: WebAuthn Assertion to EVM Verifier
Passkey smart accounts do not sign Ethereum transactions using standard secp256k1 keys. Instead, the user’s secure enclave (Apple Secure Enclave, Windows Hello TPM, or Android Titan M2) signs a SHA-256 hash composed of the clientDataJSON and authenticatorData using the NIST P-256 (secp256r1) curve.

The Two Root Failure Vectors
Vector 1: authenticatorData Flag Mismatch (The UV Bit Trap)
The authenticatorData structure contains a critical 1-byte bitfield at byte offset 32:
Byte 32 Flags:
Bit 0: UP (User Presence) -> 0x01 (Must always be 1)
Bit 1: Reserved
Bit 2: UV (User Verification) -> 0x04 (1 if biometric/PIN verified, 0 if only presence)
Bit 6: AT (Attested Credential) -> 0x40
Bit 7: ED (Extension Data) -> 0x80
When the smart contract passkey verifier executes:
// On-chain verification constraint inside WebAuthnValidator.sol
bytes1 flags = authenticatorData[32];
require((flags & 0x01) != 0, "UP bit not set"); // User Presence
if (requireUserVerification) {
require((flags & 0x04) != 0, "UV bit not set"); // User Verification (Biometric)
}
If the frontend WebAuthn assertion was requested with userVerification: "discouraged", or if an external security key without biometric capability was tapped, flags & 0x04 evaluates to 0. The contract reverts with empty return data or custom error, prompting the EntryPoint to emit AA23.
Vector 2: High-$s$ Signature Malleability ($s > N/2$)
In ECDSA cryptography, the P-256 curve order is defined as:
$$N = \text{0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551}$$
If the authenticator emits a signature where $s > N / 2$, strict smart contracts and the RIP-7212 precompile will reject the signature to prevent transaction hash malleability.
Step-by-Step Resolution Protocol
To resolve AA23 reverts across all major mobile and desktop platforms, follow this implementation pipeline.
1. Enforce userVerification: "required" in Assertion Request
Update your dApp or agent client authentication request to strictly mandate hardware biometric verification from the browser:
// src/auth/passkeyAuth.ts
export async function generatePasskeySignature(userOpHash: Uint8Array, credentialId: string) {
const challenge = userOpHash; // UserOp hash formatted as buffer challenge
const assertion = await navigator.credentials.get({
publicKey: {
challenge: challenge.buffer,
allowCredentials: [
{
id: Buffer.from(credentialId, 'base64url'),
type: 'public-key',
transports: ['internal', 'hybrid'],
},
],
// CRITICAL: Must be "required" to guarantee the 0x04 UV bit is set in authenticatorData
userVerification: 'required',
timeout: 60000,
},
}) as PublicKeyCredential;
if (!assertion) {
throw new Error('WebAuthn assertion failed or canceled by user.');
}
return assertion;
}
2. Normalize Signature to Low-$s$ Canonical Form
Before packing the (r, s) values into the UserOperation.signature field, check whether $s > N / 2$ and invert it if necessary:
// src/auth/p256Canonicalizer.ts
const P256_N = BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");
const P256_HALF_N = P256_N >> 1n;
export function normalizeP256Signature(rBytes: Uint8Array, sBytes: Uint8Array): { r: bigint; s: bigint } {
let r = BigInt('0x' + Buffer.from(rBytes).toString('hex'));
let s = BigInt('0x' + Buffer.from(sBytes).toString('hex'));
// If s is in the upper half of the curve order, flip it to the lower half
if (s > P256_HALF_N) {
s = P256_N - s;
}
return { r, s };
}
3. Verify authenticatorData Flags Client-Side Prior to Broadcast
Inspect the raw assertion buffer client-side to preempt bundler drops and give users actionable diagnostic feedback if their hardware key lacks biometrics:
// src/auth/validateAssertion.ts
export function inspectAuthenticatorData(authDataBuffer: ArrayBuffer) {
const authDataView = new DataView(authDataBuffer);
// Byte 32 holds the bitmask flags
const flags = authDataView.getUint8(32);
const userPresence = (flags & 0x01) !== 0;
const userVerified = (flags & 0x04) !== 0;
if (!userPresence) {
throw new Error('PASSKEY_ERROR: User presence flag (UP) is missing.');
}
if (!userVerified) {
throw new Error(
'PASSKEY_ERROR: User verification flag (UV) is missing. ' +
'Please use Touch ID, Face ID, or Windows Hello PIN rather than a plain security key tap.'
);
}
return { flags, userPresence, userVerified };
}
Production Diagnostic & Verification Table
| Test Scenario | Client Setting / Payload | Expected EntryPoint Result |
|---|---|---|
| Strict Biometric (TouchID/FaceID) | userVerification: 'required', Flag 0x05 (UP+UV) | simulateValidation returns success (Execution Gas estimated). |
| Plain Hardware Tap without PIN | Flag 0x01 (UP only, UV=0) | Contract reverts immediately client-side with actionable error; prevents bundler fee loss. |
| High-$s$ Signature Scalar | Unnormalized $s > N/2$ | Fails with AA23 on RIP-7212 precompile chains (Arbitrum, Optimism, Base). |
| Normalized Low-$s$ Scalar | Canonical $s \le N/2$ | Transaction included successfully on-chain. |
Frequently Asked Questions
Q: Why did the same passkey work on EntryPoint v0.6 but fail on EntryPoint v0.7?
EntryPoint v0.7 introduced tighter constraints on validation gas rules and stricter enforcement against non-canonical signature representation. Many older v0.6 contracts did not enforce low-$s$ canonicalization or permitted UV-less signatures, which modern v0.7 account implementations have hardened for regulatory and security compliance.
Q: Can I configure the smart account to allow userVerification: 'preferred'?
Yes, but you must modify the account’s on-chain validation module to check only the UP bit (flags & 0x01) and omit the strict UV bit (flags & 0x04) requirement. Note that this lowers security against physical device theft where an unauthorized actor taps a plugged-in USB key without PIN entry.
Q: Does RIP-7212 support all EVM chains?
RIP-7212 (secp256r1 precompile at address 0x0000000000000000000000000000000000000100) is deployed natively on networks like Arbitrum One, Base, and Optimism. On Ethereum mainnet or Polygon, smart accounts use highly optimized Solidity verifiers (such as Daimo’s P256 verifier) which enforce identical cryptographic checks.