soobook
ETHEREUM

Password × ERC-4337 (2026)

비밀번호 매니저가 보편화되면서 일반 웹에서는 비밀번호로 어떤 사이트든 로그인하는 흐름이 자리 잡았다. 그런데 web3 wallet은 여전히 seed phrase와 디바이스 keypair 둘 중 하나를 고르라고 한다. 비밀번호 한 줄로 wallet을 쓰는 길은 왜 그동안 닫혀 있었을까. OPAQUE 프로토콜이 그 길을 다시 연다.

ERC-4337의 validateUserOp이 시그니처 검증을 컨트랙트가 정의하도록 추상화해 두었으니, 비밀번호로 도출한 keypair를 wallet의 signer로 꽂는 그림은 어렵지 않다.

그래도 이 그림이 보안 측면에서 동작하려면 protocol 차원과 인프라 차원의 보강이 같이 필요했다. 무엇이 보강되어야 했는지 본다.

비밀번호로 키를 만들면 왜 위험한가

가장 단순한 방식부터 짚어보자. 사용자가 입력한 비밀번호를 KDF(key derivation function, 예: PBKDF2, scrypt, Argon2)에 넣어 256-bit 시드를 뽑고, 그 시드로 secp256k1 keypair를 만들면 된다.

같은 비밀번호 + 같은 KDF = 같은 키. 사용자는 비밀번호만 외우면 어떤 디바이스에서도 같은 wallet에 접근한다.

여기서 두 군데가 막힌다.

먼저, 비밀번호의 entropy가 너무 낮다. 사람이 외우는 비밀번호는 보통 30~50 bit 정도의 entropy를 가진다. 16자 random 문자열도 100 bit 안팎. 256-bit random seed에 비하면 한참 모자란다.

brute-force 시도가 충분히 빠르면 사전 공격으로 풀린다.

또 하나, 블록체인 환경에서는 brute-force가 막히지 않는다. Web2 서버는 같은 IP나 같은 계정에 5번 틀리면 잠그는 rate limit이 있다. 그래서 사람이 외우는 정도의 비밀번호로도 사실상 안전하다.

블록체인은 모든 상태가 public이고, 공격자가 자기 컴퓨터에서 KDF만 돌려 모든 비밀번호 후보의 keypair를 미리 계산해 둘 수 있다(offline brute-force). 사용자가 트랜잭션을 한 번이라도 보내면 그 주소의 public key가 노출되고, 공격자는 미리 계산해둔 사전과 대조해 비밀번호를 알아낸다.

EOA의 secp256k1 keypair는 256-bit random이라 brute-force 자체가 불가능하다. 그러나 비밀번호로 만든 keypair는 몇십 비트의 entropy로 줄어들어 “공격자가 시간만 들이면 풀린다”는 상태가 된다.

seed phrase가 12~24 단어인 이유도 같다. 사람이 외울 정도면서 brute-force를 막을 수 있는 entropy 하한이 그 근처다.

PAKE

 

비밀번호를 그대로 노출하지 않는 키 교환\text{비밀번호를 그대로 노출하지 않는 키 교환}

 

이 문제를 web2가 푼 도구 중 하나가 PAKE(Password-Authenticated Key Exchange)다. 이름 그대로 비밀번호 기반 키 교환인데, 핵심 약속은 이렇다.

비밀번호를 평문으로 네트워크에 흘리지 않으면서, 양쪽이 비밀번호를 안다는 사실에 기반해 안전한 세션 키를 만든다.

PAKE의 직관적 설명: 두 당사자(client와 server)가 같은 비밀번호 P를 가지고 있다고 가정하고, 이걸 직접 비교하지 않으면서도 공격자가 중간에서 가로채도 P를 추측할 수 없도록 키 교환을 진행한다.

가장 단순한 PAKE flow를 단계별로 보면 이렇다.

  1. client가 비밀번호를 가린 값 X를 만든다. 입력받은 비밀번호 P에 random salt와 blinding을 적용해 X를 계산한다. X에는 P가 산술적으로 섞여 있지만, blinding 값을 모르면 P를 풀어낼 수 없다.
  2. client가 X를 server에 보낸다. server 입장에서 X는 임의의 값으로 보일 뿐, 원래 비밀번호가 무엇이었는지 알 길이 없다.
  3. server가 자기 verifier V로 응답 Y를 만든다. verifier V는 사용자 가입 시점에 P로부터 도출해 보관해둔 값이다. server는 X와 V를 결합해 응답 Y와 자기 쪽 세션 키 후보를 동시에 계산한다.
  4. server가 Y를 client에 보낸다. 같은 시점에 양쪽이 각자 세션 키 K를 도출한다. client는 P와 Y로, server는 V와 X로.
  5. 양쪽의 K가 일치하는지 짧은 mac으로 확인한다. P가 맞았다면 양쪽 K가 같다. 틀렸다면 K가 다르고 mac 검증이 실패해 즉시 인증 실패로 끝난다.

여기서 server는 P를 평문으로 본 적이 한 번도 없다. server가 가진 건 verifier V뿐이고, V만으로는 P를 역산할 수 없다.

공격자가 server를 통째로 털어 V를 가져가도 P를 brute-force하려면 PAKE protocol 자체를 server와 한 번씩 돌려야 하니, server의 rate limit 안에 묶인다.

이 PAKE 가족에서 가장 견고한 인스턴스가 OPAQUE이고, 그 핵심 building block이 OPRF다.

OPRF

 

비밀번호를 blind해서 처리하기\text{비밀번호를 blind해서 처리하기}

 

PAKE가 인증과 세션 키 합의까지 책임지는 완성품이라면, OPRF는 그 안에 들어가는 building block이다.

OPRF 단독은 인증도 세션 키도 만들지 않는다. 단지 “client가 입력을 server에 노출하지 않고 함수 값 하나를 받는다”는 동작만 책임진다.

OPAQUE는 이 OPRF 위에 key recovery와 AKE를 얹어 PAKE를 완성한다. 그래서 PAKE flow 1~3단계의 산술이 OPRF에 해당하고, 4~5단계의 세션 키 합의와 mac 검증이 PAKE가 OPRF 위에 추가하는 부분이다.

OPRF(Oblivious Pseudorandom Function)는 두 당사자가 두 비밀을 서로 모르는 채로 함수 값을 계산하는 프로토콜이다. client는 input x(비밀번호)를 알지만 server의 PRF key k를 모르고, server는 k를 알지만 x를 모른다.

둘이 협력해 PRF(k, x)를 계산하되, 끝나고 나면 client만 결과를 받고 server는 input과 output 둘 다 알지 못하는 상태가 목표다.

작동 흐름은 이렇다.

  1. client가 비밀번호를 blind한다. random 난수 b를 뽑아 비밀번호 x에 적용해 blinded 값 B를 만든다. B는 x를 산술적으로 품고 있지만 b 없이는 x를 끄집어낼 수 없다.
  2. client가 B를 server에 보낸다. server 입장에서 B는 노이즈가 섞인 임의의 값일 뿐이라, 원래 비밀번호가 무엇이었는지 추측할 단서가 없다.
  3. server가 자기 PRF key로 B를 처리해 응답 Y를 만든다. 여기서 server는 자기가 무엇을 처리하는지(B의 원래 형태) 모른다. server에겐 B와 자기 key k를 결합한 산술 결과 Y만 남는다.
  4. server가 Y를 client에 돌려준다.
  5. client가 b를 사용해 Y에서 blind를 푼다. 풀린 결과가 곧 PRF(k, x)다. server의 PRF key가 적용됐고 client의 비밀번호가 입력으로 들어간, deterministic하고 high-entropy한 값.

이 흐름이 비밀번호 wallet에서 왜 중요한지 보면, server가 원본 비밀번호를 절대 보지 않으면서도 client에게 deterministic한 PRF 출력을 돌려준다는 데 있다. 출력을 keypair seed로 쓰면 비밀번호 자체의 낮은 entropy 문제가 PRF key k의 256-bit entropy로 보강된다.

같은 비밀번호 + 같은 server PRF key = 항상 같은 seed = 같은 keypair.

server가 PRF key k를 비밀로 보관하면서 매 요청마다 호출 횟수를 제한하면 offline brute-force가 막힌다. 공격자가 후보 비밀번호 사전을 가지고 있어도 server에 매번 요청해야 PRF 출력을 얻으니, server의 rate limit 안에 묶인다.

OPAQUE

 

비밀번호로 매번 같은 keypair를 복구한다\text{비밀번호로 매번 같은 keypair를 복구한다}

 

OPAQUE는 PAKE 가족 중에서도 비대칭(asymmetric) PAKE, 즉 aPAKE다. 비대칭이라는 말의 정확한 의미는 server가 비밀번호를 어떤 형태로도 알지 못한다는 뜻이다. server가 가진 건 사용자 가입 시점에 만들어진 envelope 한 덩어리뿐이고, envelope만으로는 비밀번호를 역산할 수 없다.

server가 통째로 털려도 공격자가 비밀번호 사전을 미리 OPRF에 돌려놓을 수 없으니 pre-computation 공격 저항성까지 보장된다.

OPAQUE는 OPRF + key recovery + AKE 셋의 합성이다. OPRF로 client가 비밀번호를 blind해 PRF 출력을 받고, key recovery로 그 출력을 envelope을 푸는 열쇠로 사용해 자기 long-term keypair를 복구하고, AKE(Authenticated Key Exchange)로 양쪽이 mutual auth된 세션 키를 합의한다. RFC 9807에 정확한 합성 정의가 있다.

정말 어렵다!

여기서 자연스러운 의문이 하나 든다. 비밀번호의 entropy는 30~50 bit로 낮다고 위에서 봤는데, 어떻게 같은 비밀번호로 어떤 디바이스에서든 같은 wallet에 로그인할 수 있는가? brute-force는 또 어떻게 막히는가?

OPAQUE는 비밀번호로 keypair를 직접 만들지 않는다.

대신 OPAQUE flow를 매번 돌려 같은 keypair를 다시 만들어 낸다!

flow가 끝나면 client 메모리에 결정적이고 high-entropy한 keypair가 복구된다.

같은 비밀번호 + 같은 server PRF key는 항상 같은 PRF 출력을 내고, 그게 같은 keypair로 이어진다.

차이는 여기다. keypair는 어디에도 저장되지 않는다. 디바이스 keystore에도, server에도, 컨트랙트에도 평문으로 존재한 적이 없다. wallet 앱을 닫으면 메모리에서 사라지고, 다음에 비밀번호를 입력하면 같은 OPAQUE flow가 같은 keypair를 메모리에 다시 만들어 낸다.

 

Private key는 비밀번호의 함수다\textbf{\text{Private key는 비밀번호의 함수다}}

 

OPAQUE wallet이 seed phrase wallet, 패스키 wallet과 다른 지점이 여기다. seed phrase wallet은 키를 디바이스 keystore에 저장하고 백업으로 phrase를 외우게 한다. 패스키 wallet은 키를 디바이스 secure enclave에 묶는다. OPAQUE wallet은 키를 어디에도 두지 않는다. 비밀번호와 OPAQUE 시스템만으로 매번 같은 키를 다시 만든다.

비밀번호의 낮은 entropy 문제는 server PRF key의 256-bit entropy로 보강된다. 그래서 사용자는 외울 수 있는 비밀번호 한 줄로 어떤 디바이스에서든 같은 wallet에 진입한다.

Web3에 OPAQUE를 그대로 가져올 수 없는 이유

여기까지가 web2 모델이다. server라는 신뢰할 수 있는 주체가 PRF key를 비밀로 보관하고 rate limit을 적용한다는 가정 위에 OPAQUE가 동작한다.

Web3에 그대로 deploy하면 두 가정이 동시에 깨진다.

“server” 자리에 컨트랙트를 두면 컨트랙트의 모든 상태는 public이다. PRF key k를 컨트랙트에 저장하는 순간 모든 노드가 그 값을 본다. k가 공개되면 OPRF 자체가 무너진다.

공격자가 자기 컴퓨터에서 PRF(k, x)를 모든 비밀번호 후보로 미리 계산해버리니, OPAQUE가 풀려고 했던 offline brute-force 문제가 그대로 남는다.

rate limit도 어렵다. EVM 컨트랙트가 “이 주소가 5번 호출했으면 거부”식 logic을 가질 수는 있지만, 공격자는 새 주소를 무한히 만들 수 있고 모든 OPRF 입력 조합을 시도하지 못하게 막을 도덕적 식별자가 없다.

더 본질적으로 컨트랙트가 그 logic을 갖는 동안에도 누군가 archive node에서 과거 state를 읽어 PRF key를 회수할 수 있다.

OPAQUE의 두 보안 장치(secret PRF key + rate limit)가 web3 환경에서는 protocol 차원에서 보장되지 않는다. 비밀번호 wallet이 그동안 web3에서 막혀 있던 이유다.

web3에서 OPAQUE가 마주치는 문제를 정리하면 두 가지다.

  1. PRF key 문제. OPAQUE가 동작하려면 server가 PRF key를 비밀로 보관해야 하는데, 컨트랙트 storage는 모든 노드가 읽을 수 있어 비밀 보관 자리가 없다.
  2. Rate limit 문제. OPAQUE의 brute-force 방어는 server의 호출 횟수 제한에 의존하는데, 블록체인은 누구나 새 주소로 무한히 호출할 수 있어 그 제한이 무력화된다.

Balthazar Wallet: TEE로 두 문제 풀기

Balthazar Wallet의 접근은 단순하다. PRF key 비밀 보관과 rate limit 강제가 일반 컨트랙트 환경에서 안 되는 거라면, 컨트랙트를 일반 환경 대신 TEE 안에서 실행시키면 된다.

TEE란

TEE(Trusted Execution Environment)는 CPU 안에 격리된 실행 환경이다. 일반 OS, 하이퍼바이저, 심지어 머신을 운영하는 사람조차 그 안의 코드와 데이터를 들여다볼 수 없도록 하드웨어가 보장한다.

결과만 외부로 나오고, 내부 변수와 메모리는 어떤 권한으로도 직접 읽을 수 없다.

같은 시리즈의 Passkey × ERC-4337에서 본 디바이스 secure enclave와 같은 개념이다. iPhone Secure Enclave가 디바이스 안에서 private key를 격리했다면, Intel SGX, AMD SEV, ARM TrustZone, AWS Nitro Enclaves 같은 server-side TEE는 데이터센터급 머신 안에서 같은 격리를 만든다.

PPP

이 TEE 안에서 컨트랙트를 실행하는 블록체인 플랫폼을 PPP(Privacy-Preserving smart contract Platform)라 부른다. Oasis Network, Secret Network, Phala Network 같은 체인이 여기 속한다.

PPP에서는 컨트랙트 코드와 실행 결과를 일반 블록체인처럼 모든 노드가 검증한다. 그러나 컨트랙트가 다루는 비밀 데이터(state 일부)는 TEE 밖으로 새지 않는다.

곧 노드는 “이 컨트랙트가 정상적으로 실행됐다”는 걸 검증할 수 있지만, “이 컨트랙트의 storage 안에 무슨 값이 들어 있는가”는 알 수 없다.

두 문제가 어떻게 풀리는가

먼저 PRF key. OPAQUE의 PRF key를 PPP 컨트랙트 storage에 저장한다. 일반 컨트랙트라면 이 값이 모든 노드에 공개돼 무너지지만, PPP에서는 storage 자체가 TEE 안에 있어서 외부 노드는 평문으로 읽을 권한이 없다.

컨트랙트 함수가 PRF 연산을 수행할 때만 TEE 안에서 PRF key가 사용되고, 외부에 나가는 건 연산 결과뿐이다.

다음으로 rate limit. rate limit logic을 같은 컨트랙트 안에 박는다. “이 client가 1분에 5번 이상 호출하면 거부”식 제약이 TEE 안에서 강제된다.

공격자가 새 주소를 무한히 만들어도, TEE 안의 컨트랙트는 client를 어떤 식별자(IP, 디바이스 attestation 등)로 묶을지를 자기 코드로 정의하니, 호출 주소만 갈아치워도 같은 사용자로 카운트할 수 있다.

archive node로 과거 state를 읽어 회피하려 해도 state 자체가 TEE 밖에서는 의미 없는 형태라 우회가 안 된다.

이렇게 OPAQUE의 두 가정이 PPP 환경에서 다시 성립한다. 사용자가 자기 비밀번호 + Balthazar 시스템만 알면 어떤 디바이스에서든 자기 키를 복구한다.

전체 흐름

sequenceDiagram
    actor User
    participant Client as Client (browser/app)
    participant Relay as Relay (TEE)
    participant PPP as PPP Smart Contract

    User->>Client: enter password
    Client->>Client: blind(password) → B
    Client->>Relay: B
    Note over Relay: rate limit check
    Relay->>Relay: Y = B^k (PRF key in TEE)
    Relay->>Client: Y
    Client->>Client: unblind(Y) → seed
    Client->>Client: derive keypair from seed
    Client->>PPP: register / use keypair

이 그림에서 client는 사용자 디바이스, relay는 TEE 안에서 OPAQUE 서버 역할을 하는 노드, PPP smart contract는 회수된 keypair에 대한 권한을 컨트랙트 단에서 표현하는 부분이다.

ERC-4337과의 결합

비밀번호 wallet을 4337 어카운트에 끼워 넣는 그림은 가입(어카운트 deploy)과 사용(트랜잭션 서명) 두 단계로 나뉜다.

가입 단계. 사용자가 처음 비밀번호를 정하면 client가 OPAQUE 등록 흐름을 거쳐 relay에 verifier를 등록하고, 같은 OPAQUE 출력으로 secp256k1 keypair를 도출한다.

이 keypair의 public address가 4337 Smart Account의 owner로 박힌다. 어카운트 deploy 시 constructor 또는 initializer에 owner address가 들어가는 식이다.

contract PasswordSmartAccount {
    address public owner;  // OPAQUE-derived secp256k1 address

    constructor(address opaqueDerivedOwner) {
        owner = opaqueDerivedOwner;
    }

    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData) {
        address recovered = ECDSA.recover(userOpHash, userOp.signature);
        if (recovered != owner) return SIG_VALIDATION_FAILED;
        return 0;
    }
}

사용자 입장에선 비밀번호 한 번 정하고 wallet 만들기 한 번이 끝. owner address는 어카운트 컨트랙트의 storage에 박혀 있고, 이 address에 해당하는 private key는 Balthazar relay와 비밀번호가 함께 있어야만 복구된다.

어카운트 코드 자체에는 OPAQUE에 대한 어떤 의존도 없다. 일반 secp256k1 owner를 가진 보통의 4337 어카운트와 구조가 같다.

사용 단계. 트랜잭션마다 비밀번호를 다시 받는 건 UX가 망가지므로 보통은 둘로 쪼갠다. 먼저 OPAQUE flow를 한 번 돌려 keypair를 client 메모리에 복구하고, 그 keypair로 단기 session key를 발급받는 패턴이다.

session key는 별도 secp256k1 keypair인데, 어카운트 코드에 “이 session key는 1시간 동안 이 컨트랙트에 한해서만 서명 권한”식 제약을 박아서 등록한다. 사용자는 비밀번호 입력 한 번 → 그 시간 동안 daily 트랜잭션은 session key가 자동 서명.

비밀번호와 OPAQUE relay는 session key 발급 시점에만 호출되니 UX가 가벼워진다. 이 session key 메커니즘은 4337 어카운트에서 일반화된 패턴이고 ERC-7715 같은 권한 위임 표준으로 정착되는 중이다.

owner를 두 종류로 둘 수도 있다. primary owner = OPAQUE-derived, secondary owner = passkey 또는 EOA backup. 어느 쪽이든 EIP-1271의 정책 통과 증명 추상화 안에서 같은 어카운트의 서명자로 인정된다.

전체 흐름을 단계별로 풀면:

  1. 사용자가 wallet 앱에서 비밀번호 입력
  2. client가 OPAQUE flow로 TEE relay와 상호작용해 OPAQUE seed 도출
  3. seed에서 secp256k1 keypair 복구 (가입 시 박힌 owner address와 일치)
  4. 단기 session key 발급 트랜잭션 한 번 (어카운트 storage에 session key를 권한자로 등록)
  5. daily 트랜잭션: UserOp 생성 → session key로 서명 → Bundler → EntryPoint → Account의 validateUserOp 통과
  6. session 만료 시 1번부터 다시

비밀번호 + OPAQUE 페어 덕에 어카운트의 마스터 권한자가 디바이스에서 분리된다. 디바이스를 잃어도 다른 디바이스에서 같은 비밀번호로 같은 keypair를 복구해 어카운트에 다시 진입한다.

한계

TEE의 trust 모델. TEE는 일반 EVM의 합의 기반 보안과 다른 trust model을 갖는다. 이더리움이 검증을 모든 노드의 합의로 보장하는 데 비해, TEE는 Intel, AMD, ARM 같은 하드웨어 제조사가 격리를 제대로 구현했다는 가정에 trust root가 있다. side-channel 공격으로 격리가 깨진 사례가 주기적으로 누적되어, 이 가정은 수학적 가정만큼 견고하지 않다. TEE가 깨지면 PRF key가 노출되고 OPAQUE의 보안 가정이 함께 무너진다.

복구 경로 부재. Web2 서버는 이메일이나 SMS로 password reset을 제공하지만, OPAQUE 시스템은 비밀번호 외에 사용자를 인증할 수단이 없다. 비밀번호를 잃으면 wallet 접근도 함께 끊긴다.

그래서 실제 운영에서는 multi-key(비밀번호 + 패스키 + recovery contact)가 권장된다.

정리

Web2의 비밀번호 모델이 지금까지 web3에 그대로 옮겨지지 못한 건 secret PRF key와 rate limit, 두 가정이 public 블록체인에서 자연스럽게 성립하지 않은 탓이다. OPAQUE가 RFC 9807으로 표준화되면서 protocol 자체는 견고해졌고, Balthazar Wallet 같은 설계가 TEE와 PPP를 끌어와 그 두 가정을 web3 환경에서 다시 세운다.

여전히 mainstream 단계는 아니다. TEE의 신뢰 가정이 풀어야 할 숙제로 남아 있다.

ERC-4337의 validateUserOp이 시그니처 검증을 코드로 추상화해 둔 덕에, 비밀번호 wallet이 실용 단계에 진입하면 같은 어카운트에서 패스키, 하드웨어 wallet, 분산 서명과 함께 한 owner로 공존할 수 있다.

비밀번호로 web3에 들어오는 길이 막혀 있던 건 protocol이 게을러서가 아니라 환경이 비대칭이라서였다.

References