Passkeys bring WebAuthn into wallet UX. A device-held credential signs a browser challenge, then wallet logic decides whether that assertion authorizes an onchain action.

Two mismatches matter. The first is the key algorithm: most passkeys use ES256, which means ECDSA over P-256, while Ethereum EOAs use ECDSA over secp256k1 and derive the account address through ecrecover. The second is the signed artifact: a passkey signs WebAuthn bytes, not a native chain transaction. A passkey keypair cannot become an Ethereum EOA key, but an account can choose to trust a WebAuthn assertion.

The WebAuthn artifact

During authentication, the authenticator signs:

TXT
authenticatorData || sha256(clientDataJSON)

The wallet controls the challenge inside clientDataJSON. It can bind that challenge to a transaction digest, typed-data digest, session request, or account operation. The signature still covers WebAuthn data: origin-bound client data, authenticator flags, an RP ID hash, and a counter.

Useful verification has two layers:

  • Algorithm check: ES256 verifies a P-256 ECDSA signature; RS256 verifies an RSA signature over SHA-256.
  • Wallet policy: challenge construction, origin, RP ID, flags, replay boundary, account nonce, chain ID, session scope, and recovery.

The cryptographic check proves that the credential signed the WebAuthn bytes. The wallet policy decides whether those bytes authorize asset movement.

Compatibility at a glance

The useful comparison is not the signature bytes. It is whether a key shape can exist as a WebAuthn credential. Ethereum EOAs and Solana native accounts use chain-native key types; passkeys expose WebAuthn algorithms such as ES256, and less commonly RS256. The next section expands those labels into the curves, signature schemes, and chain rules behind them.

Passkey compatibility
WebAuthn key shape vs native chain key
2 / 4
Ethereum EOA
secp256k1 / ecrecover
No
Native accounts expect a secp256k1 signature.
Solana
Ed25519
No
The native signer is not a WebAuthn credential.
Passkey ES256
P-256 / SHA-256
Yes
The common passkey shape on phones and laptops.
Passkey RS256
RSA / SHA-256
Rare
A WebAuthn algorithm, but many authenticators reject it.

Passkey algorithms vs chain signatures

WebAuthn does not define "a passkey algorithm" as one universal curve family. The credential public key is encoded as a COSE_Key, and the key carries a concrete alg value such as -7 for ES256 or -257 for RS256. That alg is a full signing profile: key family, signature scheme, hash function, and sometimes encoding expectations.

That is why both ES and RS exist. FIDO had to support real authenticator hardware that already came from different security lineages. Some authenticators were built around elliptic-curve keys because they are compact and cheap to move through constrained transports. Other enterprise and smart-card style systems already had RSA hardware, PKI tooling, certification paths, and verification infrastructure. WebAuthn inherited that interoperability requirement rather than pretending every authenticator would be the same kind of key.

This also explains the naming. ES256 is not an abstract "elliptic curve" class. It is the JOSE/COSE convention for ECDSA plus SHA-256 on a specific curve, with the curve itself stored in the COSE key parameters. RS256 is RSA PKCS#1 v1.5 plus SHA-256. The names identify the whole verification contract so that a relying party knows exactly how to parse the public key and verify the signature. A looser class name like EC would still need a curve, hash, signature encoding, and public-key representation before it could be verified.

There is some history in that shape. Earlier FIDO UAF registries named very specific algorithm-and-encoding constants such as ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW and RSA SHA-256 variants, partly to keep small authenticators simple and unambiguous. WebAuthn/FIDO2 moved the wire format to CBOR and reused COSE algorithm identifiers, but kept the same basic idea: algorithm negotiation is about exact suites, not broad cryptographic families. Later registrations such as RFC 8812 added more WebAuthn COSE algorithms, including secp256k1, but that still does not make Ethereum EOA signing a normal passkey shape.

ES256 is ECDSA with SHA-256 on the NIST P-256 curve. This is the common passkey shape on phones and laptops. The public key is a P-256 point (Qx, Qy), and the signature is an ECDSA pair (r, s). The signature algorithm is familiar to Ethereum developers, but the curve is not the one Ethereum EOAs use.

P-256 vs secp256k1

P-256 and secp256k1 are both 256-bit prime-field elliptic curves, and both can be used with ECDSA. That similarity is exactly what makes the mismatch easy to underestimate: ECDSA is the signing algorithm, but the curve fixes the group in which the public key and signature relation live.

P-256
y² = x³ - 3x + b
secp256k1
y² = x³ + 7

The curve is not just a drawing. It defines the finite-field group used for point arithmetic: the field prime, curve coefficients, generator point, and group order. A private scalar only becomes a public key after multiplication by that curve's generator: Q = dG. The Elliptic Curves article walks through that point-addition geometry in more detail. Change the curve and G, the point addition rules, and the valid point set change with it, so the same scalar lands on a different public key.

ECDSA signatures are checked inside that same group. The verifier rebuilds a point relation from r, s, the message hash, the generator, and the public key. If the public key lives on P-256, the relation must be verified on P-256; if it lives on secp256k1, it must be verified on secp256k1. That is why a P-256 passkey signature cannot be fed into Ethereum ecrecover: Ethereum expects the secp256k1 group, then derives the address from the recovered secp256k1 public key.

The security target is roughly the same: about 128-bit classical security. The difference is operational. P-256 won the platform-authenticator ecosystem because it was already standardized, widely certified, and implemented in secure hardware. secp256k1 won the crypto-wallet ecosystem because Bitcoin picked it, Ethereum inherited that account model, and the EVM exposes ecrecover for that curve.

A key or signature from one curve cannot be reinterpreted on the other. A P-256 public key is not a secp256k1 public key; a P-256 ECDSA signature cannot recover an Ethereum address; and Ethereum address derivation expects the Keccak hash of a secp256k1 public key. Even if a WebAuthn registry can name secp256k1 as a COSE algorithm, a normal passkey assertion still signs WebAuthn bytes, not a native Ethereum transaction.

RS256 is RSA PKCS#1 v1.5 with SHA-256. The key material is a large RSA modulus and exponent: n and e. Verification checks a modular exponentiation relation, not an elliptic-curve point multiplication. Ethereum can perform modular exponentiation through the EIP-198 precompile, so RSA verification is possible in principle. It is a poor wallet primitive: larger keys, larger inputs, heavier verification, and little account-wallet ecosystem momentum.

Native Ethereum accounts were designed for secp256k1:

  1. A transaction is hashed according to the Ethereum signing rules.
  2. A secp256k1 ECDSA signature signs that hash.
  3. ecrecover recovers the public key and therefore the EOA address.

ES256 misses that native path on three points: the curve is P-256, the public key has no Ethereum address derivation, and the WebAuthn signature wraps browser metadata around the challenge. RS256 fits even less naturally because RSA has no ecrecover-style account recovery story. A native EOA transaction expects a secp256k1 signature; ES256 and RS256 need contract or account-level verification.

Passkeys therefore enter Ethereum through account logic. ERC-4337 accounts, EIP-7702 delegation, and Safe-style modules can define a validator that accepts WebAuthn assertions.

How AA wallets use passkeys

An AA wallet does not turn the passkey into an EOA key. It stores the passkey credential as an account permission, then asks the account contract whether a WebAuthn assertion is valid for the operation being submitted.

Registration records the credential ID, P-256 public key, RP ID hash, and account policy. When the user acts, the wallet hashes an ERC-4337 UserOperation, an ERC-1271 message, or an account-specific request, then places that hash inside the WebAuthn challenge.

The browser returns authenticatorData, clientDataJSON, and a signature. The account validator checks that WebAuthn wrapper, verifies the P-256 signature against the stored passkey public key, and then applies account policy. That is why the passkey controls the wallet without becoming the wallet's native chain key: the browser sees WebAuthn; the chain sees account-level validation.

Implementation sketch

For an ERC-4337 account, the hook is validateUserOp. For dapps that verify a smart-wallet signature, the hook is often ERC-1271 isValidSignature. Both can delegate to the same passkey validator:

TS
// Browser side: bind the account operation to the WebAuthn challenge.
const userOpHash = await entryPoint.getUserOpHash(userOp)
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: userOpHash,
    userVerification: 'required',
    allowCredentials: [{ type: 'public-key', id: credentialId }],
  },
})
 
userOp.signature = encodeWebAuthnAssertion(assertion)
await bundler.sendUserOperation(userOp)

The Solidity side is easier to read when split by responsibility. The snippets below belong to the same validator contract. Production code still needs a real clientDataJSON parser, origin checks, replay protection, and chain-specific domain separation. P256.verify below means a RIP-7212-style precompile path or a verifier library, not native Ethereum ecrecover.

Stored permission. Registration stores the WebAuthn relying-party hash and the passkey public key. The signature payload later carries the WebAuthn assertion fields.

SOLIDITY
bytes32 public rpIdHash;
bytes32 public qx;
bytes32 public qy;
 
struct WebAuthnSig {
    bytes authenticatorData;
    bytes clientDataJSON;
    bytes32 r;
    bytes32 s;
}

ERC-4337 entry. A bundler submits a UserOperation; the account validator receives the userOpHash and checks whether the passkey assertion authorized that exact hash.

SOLIDITY
function validateUserOp(
    bytes32 userOpHash,
    WebAuthnSig calldata sig
) external view returns (bool) {
    return _validatePasskey(userOpHash, sig);
}

ERC-1271 entry. Dapps that ask a smart wallet to validate a message can use the same validator through isValidSignature.

SOLIDITY
function isValidSignature(
    bytes32 messageHash,
    WebAuthnSig calldata sig
) external view returns (bytes4) {
    return _validatePasskey(messageHash, sig)
        ? bytes4(0x1626ba7e)
        : bytes4(0xffffffff);
}

WebAuthn core check. This is the boundary where browser-scoped WebAuthn bytes become account authorization. First bind the clientDataJSON.challenge to the account operation, then rebuild the WebAuthn digest and verify it against the stored P-256 public key.

SOLIDITY
function _validatePasskey(
    bytes32 expectedChallenge,
    WebAuthnSig calldata sig
) internal view returns (bool) {
    if (!WebAuthn.challengeMatches(sig.clientDataJSON, expectedChallenge)) {
        return false;
    }
    if (_rpIdHash(sig.authenticatorData) != rpIdHash) return false;
    if (!_userPresentAndVerified(sig.authenticatorData)) return false;
 
    bytes32 clientHash = sha256(sig.clientDataJSON);
    bytes32 webAuthnDigest = sha256(
        bytes.concat(sig.authenticatorData, clientHash)
    );
 
    return P256.verify(webAuthnDigest, sig.r, sig.s, qx, qy);
}

Authenticator-data helpers. authenticatorData starts with the RP ID hash. The flags byte follows at offset 32; bit 0 means user presence and bit 2 means user verification.

SOLIDITY
function _rpIdHash(
    bytes calldata authenticatorData
) internal pure returns (bytes32 out) {
    if (authenticatorData.length < 37) return bytes32(0);
    assembly {
        out := calldataload(authenticatorData.offset)
    }
}
 
function _userPresentAndVerified(
    bytes calldata authenticatorData
) internal pure returns (bool) {
    if (authenticatorData.length < 37) return false;
    uint8 flags = uint8(authenticatorData[32]);
    return (flags & 0x01) != 0 && (flags & 0x04) != 0;
}

The important part is not the interface name. It is the boundary: WebAuthn proves that the passkey signed browser-scoped bytes; account logic decides whether those bytes authorize a chain operation.

P-256 on EVM

RIP-7212 changes the economics by adding a cheap P-256 verification path. OP-Stack Fjord and Polygon Napoli are examples of that direction. This does not make passkeys native Ethereum keys; it only makes the curve check practical inside account logic.

The precompile answers one narrow question: is this P-256 signature valid for this digest and public key? Challenge binding, RP ID, origin, nonces, sessions, and recovery remain wallet design.

SOLIDITY
// Curve check only. Production code still validates the WebAuthn wrapper.
address constant P256VERIFY = 0x0000000000000000000000000000000000000100;
 
function verifyP256(
    bytes32 digest,
    bytes32 r,
    bytes32 s,
    bytes32 Qx,
    bytes32 Qy
) internal view returns (bool ok) {
    bytes memory input = abi.encodePacked(digest, r, s, Qx, Qy);
    uint256 success;
    assembly {
        success := staticcall(gas(), P256VERIFY, add(input, 0x20), mload(input), 0, 0)
    }
    return success == 1;
}

Solana takes another route

Solana transactions require Ed25519 signatures. Mainstream browser passkeys mostly give wallets ES256, so Solana integrations usually treat passkeys as authorization for a separate signer:

TXT
passkey assertion -> session policy -> Ed25519 signer -> Solana transaction

That signer might be MPC, TEE-backed, hosted, app-controlled, or implemented inside another account abstraction layer. The security review moves to session scope, duration, spending limits, custody, recovery, and how the Ed25519 key is protected.

Projects and where they fit

These projects differ mostly by where the passkey stops and where chain-native authorization begins.

EVM smart-account path

  • Coinbase Smart Wallet: passkeys as a smart-wallet signing experience.
  • Safe: passkey validation through account modules and ERC-1271-style checks.

Cheap P-256 verification path

  • OP-Stack Fjord: one of the P-256 verification paths that makes ES256 practical on OP chains.
  • Polygon Napoli: shipped RIP-7212 support on Polygon PoS.

Non-EVM authorization path

  • Para: uses passkeys as an authorization layer for Solana signing.
  • NEAR: shows a more account-native passkey path through NEAR Intents.

The useful mental model is simple: passkeys are not chain-native private keys. They are origin-bound WebAuthn credentials that wallet or account logic can choose to trust.