soobook
ETHEREUM

Passkey × ERC-4337 (2026)

아이폰 유저가 어떤 사이트에 처음 들어가면 비밀번호도, 이메일 인증도 없이 Face ID 한 번으로 가입이 끝나는 경험을 할 수 있다. 일반 웹에서는 패스키가 비밀번호를 빠르게 대체하는 중이다.

wallet 입장에서 흥미로운 건 패스키가 풀려는 문제가 ethereum onboarding이 풀지 못한 문제와 거의 같다는 것이다.

사용자가 12~24 단어 seed phrase를 직접 백업하고 분실하지 않을 책임을 진다는 구조는 일반 웹의 비밀번호 모델과 같은 결함을 안고 있다.

ERC-4337의 validateUserOp이 서명 검증을 컨트랙트 코드 안으로 끌어와 두었으니, 이론적으로는 wallet이 어떤 시그니처 스킴이든 받아들일 수 있다.

그러나 패스키 wallet이 현재 시점에 신규 사용자 onboarding의 사실상 표준으로 자리 잡은 건 이 이론만으로는 부족했다.

protocol 차원에서 secp256r1 precompile이 함께 들어왔을 때부터 실제로 쓸 만해졌다.

How Passkeys Work

도입부에서 패스키가 비밀번호를 대체한다고 했지만, 정확히 어떤 메커니즘인지 짚고 가야 한다.

WebAuthn(Web Authentication)은 브라우저와 인증기 사이의 API다.

여기서 말하는 인증기는 디바이스에 내장된 Touch ID/Face ID 같은 생체 인증 모듈이거나, 외부 보안 키다.

정리하면 이렇다.

  1. 사용자가 인증기로 생체 인증(Face ID, Touch ID 등)한다.
  2. 인증기가 브라우저와 통신한다.
  3. 브라우저는 WebAuthn API로 사이트나 wallet 앱과 통신한다.
  4. 사이트나 wallet 앱은 WebAuthn API로 인증기에 challenge를 보내고, 서명받은 결과를 자기 서버나 컨트랙트로 검증한다.

각 레이어 역할:

  • 인증기: keypair 생성, 보관, 서명. 디바이스 안의 격리된 보안 영역에 살고, private key는 절대 밖으로 나오지 않는다.
  • 브라우저: WebAuthn API를 노출.
  • 사이트 또는 wallet 앱: 인증기에 challenge를 보내고, 서명받은 결과를 자기 서버나 컨트랙트로 검증한다.

여기서 하나 짚고가자. 서버나 컨트랙트에서 “검증”을 한다는 것은, 서명을 가지고 유저를 특정할 수 있다는 것이다.

가입과 로그인이 다음과 같이 이루어질 것이다.


가입(registration)

사이트가 인증기에 “이 사이트용 keypair 만들어줘”를 요청한다. 인증기는 곡선 secp256r1(=P-256)으로 keypair를 생성한 뒤 public key와 credential ID를 반환한다.

사이트는 이 public key를 사용자 record에 저장한다.

로그인(authentication)

사이트가 인증기에 “이 challenge를 자기 private key로 서명해줘”를 요청한다.


인증기는 사용자 인증(Face ID 등) 후 서명한다.

사이트는 저장해둔 public key로 서명을 검증한다.

비밀번호 모델과 핵심 차이는 둘이다. private key는 디바이스를 떠나지 않고, 서명은 challenge에 묶이므로 replay 공격이 통하지 않는다.

Why Passkeys

EOA의 가장 큰 사용자 측 마찰은 secp256k1 keypair를 사용자 본인이 직접 관리해야 한다는 데 있다.

seed phrase 12~24 단어 백업, 분실하지 않을 책임. 신규 사용자가 가장 자주 막히는 단계가 여기다.

패스키는 이 책임을 디바이스와 OS 차원으로 옮긴다. 사용자는 자기 얼굴이나 지문으로 인증하면 끝이고, 백업은 iCloud Keychain, Google Password Manager, 1Password 같은 OS 차원 sync가 처리한다.

신규 사용자 입장에서 “wallet을 만든다”는 mental model 자체가 사라진다. 사이트에 가입하듯 Face ID 한 번이면 wallet이 그 자리에서 만들어진다.

여기에 한 조건이 따라붙는다. 패스키도 ECDSA를 쓰지만 곡선이 다르다. WebAuthn 표준은 secp256r1(NIST P-256)이고, 이더리움 EOA가 쓰는 secp256k1과는 곡선 파라미터만 다른 같은 알고리즘이다.

ECDSA, secp256k1, P-256 곡선 자체의 수학(타원곡선 위의 점 덧셈, 모듈러 연산)은 별도 글에서 다룬다. 여기서는 “둘 다 ECDSA지만 곡선 파라미터가 달라 같은 코드로 검증되지 않는다” 정도의 사실만 짚고 간다.

Why the EVM Can’t Verify P-256 Natively

“EVM은 튜링 완전한데 P-256 검증 코드도 그냥 솔리디티로 짜면 되는 게 아니냐” 라는 의문이 자연스럽다.

그게 가능하다면 애초에 디바이스나 OS에 의존하지 않고 온체인에서 검증도 할 수 있을것만 같다.

한 마디로 일축하면 “된다”

짤 수는 있다. P-256 verify는 결국 타원곡선 위의 점 산술과 모듈러 연산이고, 솔리디티는 256-bit 정수 연산을 지원하니 라이브러리로 구현 가능하다.

실제로 Daimo p256-verifierFreshCryptoLib 같은 솔리디티 P-256 verifier가 존재한다.

그런데 비싸다. P-256 verify 한 번이 EVM에서 솔리디티로 돌면 약 330,000~700,000 gas가 든다. 트랜잭션 한 건이 이 정도면 일상 사용은 막힌다.

비교: secp256k1 검증은 EVM의 ecrecover precompile로 약 3,000 gas다.

What Is a Precompile

EVM의 모든 트랜잭션은 모든 풀노드가 똑같이 실행하고 같은 결과에 도달해야 한다(합의 요건). 솔리디티로 keccak256 호출 한 줄을 적으면 그 코드는 EVM bytecode로 컴파일되어 모든 노드의 EVM 안에서 똑같이 실행된다.

EVM bytecode는 작은 opcode 단위(ADD, MUL, MLOAD, SHA3 등)로 구성되고 각 opcode마다 gas 비용이 정해져 있다.

P-256 verify를 솔리디티로 짜면 곡선 점 덧셈, 곱셈, 모듈러 역원 같은 연산이 EVM opcode 수만 개로 풀려 실행된다. 그래서 비싼 거다.

precompile은 protocol이 자주 쓰이는 무거운 함수를 EVM bytecode 밖에서 native 코드(보통 노드 구현체인 Geth, Erigon, Reth의 Go 또는 Rust 구현)로 미리 박아둔 함수다.

호출은 보통 컨트랙트와 똑같이 staticcall로 하지만, 안에서는 EVM opcode 한 발 한 발이 아니라 노드 구현의 native 라이브러리가 직접 계산한다. gas는 그 함수 자체의 산술 비용으로 책정된다 (예: secp256k1 검증 3,000 gas, sha256 60 + 12n gas).

지금까지 EVM에 박혀 있던 precompile들:

  • 0x01 ecrecover (secp256k1 ECDSA 검증)
  • 0x02 sha256
  • 0x03 ripemd160
  • 0x04 identity (memcpy)
  • 0x05 modexp
  • 0x06 ~ 0x09 BN254 곡선 페어링 (zk 증명 등)
  • 0x0a blake2f

새 precompile은 protocol 변경(하드포크)을 거쳐야 추가된다. 모든 노드 구현(Geth, Erigon, Reth, Nethermind, Besu)이 같은 결과를 내도록 spec과 구현을 합의해야 하기 때문이다.

이 비용 때문에 새 곡선 하나를 native로 추가하는 일이 신중해진다.

RIP-7212 and EIP-7951

P-256 검증을 protocol 차원에서 푸는 시도는 두 줄기로 진행됐다.

RIP-7212(Rollup Improvement Proposal)는 L2 rollup 표준으로, secp256r1 verify 함수를 precompile (주소 0x0100, gas 약 3,450)로 제공한다. 메인넷보다 protocol 변경 부담이 덜한 L2가 먼저 움직였다.

zkSync, Polygon zkEVM, Arbitrum, Optimism, Base가 차례로 활성화했다.

EIP-7951은 메인넷용으로, RIP-7212의 보안 edge case를 보완한 같은 인터페이스다. Fusaka 하드포크에서 메인넷 활성화됐다.

이로써 메인넷과 주요 L2 모두 P-256 검증을 약 3,500 gas에 처리한다.

precompile이 활성되지 않은 환경에서는 솔리디티 라이브러리로 폴백한다. Coinbase의 webauthn-sol이 표준 패턴을 정의한다.

precompile을 먼저 호출하고, 실패하면 FreshCryptoLib 또는 Daimo p256-verifier로 fallback. fallback gas는 330k~700k 수준이라 비싸지만 동작은 한다.

WebAuthn Signature Structure

P-256 검증 인프라가 protocol 차원에 들어왔으면 wallet은 그저 P-256 verify를 호출하면 끝일 것 같지만, 한 단계가 더 있다. WebAuthn assertion은 raw P-256 시그니처보다 한 겹 더 감싸져 있다.

assertion 구성:

  • clientDataJSON: 브라우저가 본 인증 요청 메타데이터. {type, challenge, origin, ...} JSON. challenge에는 wallet이 넘긴 값(보통 userOpHash)이 들어간다.
  • authenticatorData: 인증기가 만든 데이터. rpIdHash(사이트 도메인 해시), flags, signCount 포함.
  • signature: ECDSA P-256 서명. 서명 대상 메시지는 authenticatorData || sha256(clientDataJSON).

검증은 네 단계다.

  1. clientDataJSON을 파싱해 challenge가 wallet이 기대한 값인지 확인.
  2. clientDataHash = sha256(clientDataJSON) 계산.
  3. signedMessage = authenticatorData || clientDataHash 조립.
  4. P-256 verify(signedMessage, signature, ownerPublicKey).

WebAuthn 인증기는 ECDSA 시그니처를 DER 인코딩으로 반환하는데, EVM precompile은 raw r || s (64 bytes) 형식을 받는다.

또 ECDSA 시그니처는 (r, s)(r, n-s) 두 형태가 동등한데, malleability 방지를 위해 lower-s normalize가 관행이다. webauthn-sol 같은 라이브러리가 이런 변환을 자동으로 처리한다.

Passkey Verification in validateUserOp

ERC-4337 어카운트의 validateUserOp이 패스키 검증을 어떻게 끼워 넣는지 보자.

struct WebAuthnAuth {
    bytes authenticatorData;
    string clientDataJSON;
    uint256 challengeIndex;  // challenge 위치
    uint256 typeIndex;       // type 위치
    uint256 r;
    uint256 s;
}

function validateUserOp(
    UserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external returns (uint256 validationData) {
    WebAuthnAuth memory auth = abi.decode(userOp.signature, (WebAuthnAuth));
    bool valid = WebAuthn.verify({
        challenge: abi.encode(userOpHash),
        requireUV: false,
        webAuthnAuth: auth,
        x: ownerPubKeyX,
        y: ownerPubKeyY
    });
    if (!valid) return SIG_VALIDATION_FAILED;
    return 0;
}

WebAuthn.verify 안에서 precompile call(성공 시 ~3.5k gas) → 실패 시 솔리디티 라이브러리 fallback(~330k gas) 분기가 일어난다. wallet 코드 입장에선 EIP-1271의 isValidSignature 추상화를 그대로 쓴다.

어카운트 자기 코드가 정의하는 정책 통과 증명에 P-256 시그니처가 끼어드는 것뿐이다.

여기서 owner를 두 종류로 둘 수도 있다. EOA 주소(secp256k1)와 패스키 public key(secp256r1) 양쪽을 어카운트가 알고 있고, 어느 쪽이든 서명 가능한 구조.

Coinbase Smart Wallet의 default 구성이 이 패턴이다. EOA owner는 backup, 패스키 owner는 daily use.

User Flow

실제 사용자 흐름은 단순하다.

sequenceDiagram
    actor User
    participant App as Wallet App (browser)
    participant Auth as Authenticator
    participant Bundler
    participant EntryPoint
    participant Account as Smart Account

    User->>App: tap "Send"
    App->>App: build UserOp, compute userOpHash
    App->>Auth: navigator.credentials.get(userOpHash as challenge)
    Auth->>User: Face ID prompt
    User->>Auth: face / fingerprint
    Auth->>App: WebAuthn assertion
    App->>Bundler: submit UserOp(signature = WebAuthnAuth)
    Bundler->>EntryPoint: handleOps()
    EntryPoint->>Account: validateUserOp()
    Account->>Account: WebAuthn.verify (precompile)
    EntryPoint->>Account: execute calldata

사용자 시점에서 Face ID 한 번이 곧 트랜잭션 서명이다. seed phrase 입력도, gas ETH 보유도, wallet extension 설치도 없다.

4337 paymaster가 gas까지 sponsor하면 onboarding 마찰이 거의 0에 가깝다.

Limitations

패스키 wallet이 만능은 아니다. 함정이 세 가지 있다.

디바이스 묶임. 패스키는 처음 만든 디바이스에 잠긴다. iCloud Keychain은 Apple 기기 사이만 sync한다.

디바이스 분실은 키 분실과 같다. 백업 sync가 안 된 패스키를 분실하면 wallet 접근이 끊긴다. 그래서 표준 패턴은 multi-owner다.

패스키 + EOA backup, 또는 패스키 N개를 여러 디바이스에 흩뿌리는 식. EIP-1271의 정책 통과 증명 추상화 덕에 어카운트는 N-of-M multi-key를 자유롭게 정의한다.

OS 차원 검열 가능성. Apple, Google, Microsoft가 인증기 정책을 정한다. OS가 특정 wallet 또는 특정 dapp의 패스키 호출을 막을 가능성이 이론적으로 열려 있다.

trade-off로 받아들이는 추세지만, 검열 저항 측면에선 EOA보다 약하다.

Summary

WebAuthn은 일반 웹에서 비밀번호를 대체하면서 사용자 측 키 관리를 OS 차원으로 옮겼다. 그 메커니즘이 ethereum wallet으로 넘어오려면 protocol에 secp256r1 precompile(L2 RIP-7212, 메인넷 EIP-7951)이 필요했고, 그게 활성된 시점부터 패스키 wallet이 실용 단계로 들어갔다.

ERC-4337의 validateUserOp은 시그니처 검증 로직을 코드가 정의하도록 추상화해 두었다. 이 추상화 덕에 wallet implementation은 P-256 verifier 라이브러리 한 조각만 갈아끼우면 된다.

References