서명하는 순간, 키는 반드시 존재한다
Wallet SDK를 만들면서 키 위생에는 나름 신경을 써왔다. 개인키는 Keystore V3로 암호화해서 보관하고, 시드·파생 키 같은 중간 시크릿은 사용 직후 zeroize로 메모리에서 덮어써 지운다. 그런데 이 방어들에는 공통의 한계가 있다. 서명하려면 완전한 키가 반드시 한 번은 메모리에 로드된다. 암호화는 “복호화하는 순간”을, zeroize는 “지우기 전까지의 찰나”를 남긴다. 노출 창을 줄일 수는 있어도 0으로 만들지는 못한다.
그 순간 자체를 없앨 수 있는 기술이 있다고 해서 리서치를 했다. MPC-TSS다. 이 글은 그 과정에서 정리한 개념들이다.
무엇인가
- MPC(Multi-Party Computation): 여러 참가자가 각자의 비밀 입력을 공개하지 않은 채 공동으로 하나의 결과를 계산하는 암호 기법
- TSS(Threshold Signature Scheme): MPC를 서명에 특화한 것.
t-of-n파티가 협력해야 유효한 서명 하나가 나온다
출발점은 DKG(Distributed Key Generation, 분산 키 생성)다. 완전한 키를 만든 뒤 나누는 게 아니라, 처음부터 각 파티가 자기 지분만 만든다.
DKG:
파티 A → 지분 a 생성 (다른 지분을 모름)
파티 B → 지분 b 생성 (다른 지분을 모름)
파티 C → 지분 c 생성 (다른 지분을 모름)
→ 세 지분이 수학적으로 하나의 공개키에 대응하지만,
완전한 개인키 d 는 어디에도 조립된 적이 없다
→ 그 공개키에서 EOA 주소가 결정된다
서명도 마찬가지다. 지분을 모으는 게 아니라, 각 파티가 자기 지분으로 부분 서명 기여분을 계산하고 그것들을 결합해 최종 서명 (r, s)를 만든다. 2-of-2(내 장치 + 서버) 기준으로 시퀀스를 그리면 이렇다.
sequenceDiagram
participant A as 파티 A (내 장치, 지분 a)
participant B as 파티 B (서버, 지분 b)
Note over A,B: 서명 대상 — 메시지 해시 z
Note over A,B: 라운드 1 — nonce 합의
A->>A: k의 지분 k_a 생성
B->>B: k의 지분 k_b 생성
A->>B: k_a 커밋
B->>A: k_b 커밋
Note over A,B: 라운드 2 — 곱셈 항 분산 계산
A->>B: 암호화된 중간값 (MtA / OT)
B->>A: 암호화된 중간값 (MtA / OT)
Note over A,B: k⁻¹·d 같은 곱셈 항을 지분끼리 계산
Note over A,B: 라운드 3 — 부분 서명 교환
A->>A: 부분 서명 s_a 계산
B->>B: 부분 서명 s_b 계산
A->>B: s_a
B->>A: s_b
Note over A,B: 결합 — s = s_a + s_b (mod n) → 최종 서명 (r, s)
각 라운드에서 오가는 것은 커밋·암호화된 중간값·부분 서명이지, 지분 자체가 아니다. 그래서 어느 파티도, 네트워크를 도청하는 누구도 완전한 키 d를 재구성할 수 없다. 결합된 (r, s)는 일반 ECDSA 서명과 동일해서 체인에 그대로 제출된다. (라운드의 정확한 구성과 횟수는 프로토콜마다 다르다 — 위는 개념도다.)
그런데 산출물은 평범한 secp256k1 ECDSA 서명이다. 온체인에서는 일반 EOA 트랜잭션과 구분이 불가능하다. 컨트랙트 배포도, 추가 가스비도 없고, 모든 EVM 체인에서 단일 EOA 주소로 동작한다. 이게 MPC-TSS의 세일즈 포인트다.
개념 1 — 나누는 “시점”이 다르다: Shamir vs MPC
가장 헷갈렸던 부분이다. Shamir Secret Sharing(SLIP-39)도 키를 지분으로 나누고, MPC-TSS도 지분을 쓴다. 뭐가 다른가. 나누는 시점과 합치는 방식이 다르다.
Shamir: 완성된 키 → 조각으로 분할 → 각자 보관
복구하려면 조각을 모아 재조립 → 완전한 키가 다시 등장
MPC-TSS: DKG → 완전한 키가 애초에 만들어지지 않음
각 파티가 부분 서명 계산 → 결합 → 서명 완성. 키는 끝까지 미등장
Shamir는 재조립 순간에 단일 실패점이 부활한다. 그래서 Shamir는 백업·복구 도구고(평소엔 조각으로, 복구할 때만 재조립), MPC-TSS는 상시 서명 운영 방식이다. 대체재가 아니라 서로 다른 문제를 푼다. “잃어버려도 복구되게”는 Shamir가, “서명할 때 안 새게”는 MPC가 담당한다.
개념 2 — 공짜가 아니다: 서명이 협력 이벤트가 된다
완전한 키가 없다는 말은, 뒤집으면 혼자서는 아무것도 못 한다는 말이다. 서명마다 t개 파티가 실시간으로 다중 라운드 통신을 주고받아야 한다. 단일 키의 로컬 서명처럼 오프라인에서 즉시 서명하는 것이 불가능하고, 서명 지연은 네트워크 왕복과 가장 느린 파티의 응답 속도에 종속된다.
가용성도 서명 성공의 전제 조건이 된다. 2-of-2(내 장치 + 서버) 구성이면 서버가 죽는 순간 서명도 죽는다. 그래서 실무 구성은 2-of-3(장치 + 서버 + 복구 지분)처럼 n을 늘려 특정 파티가 빠져도 다른 조합으로 서명할 수 있게 한다. t와 n은 보안(공모 저항)과 가용성(파티 손실 대비)을 맞바꾸는 다이얼이다.
키 라이프사이클도 단일 키보다 복잡해진다. 백업은 지분 단위로 해야 하고, 파티 구성이 바뀌면 지분 재분배(reshare)가 필요하다. “니모닉 12단어 적어두세요”로 끝나던 UX가 통째로 달라진다.
개념 3 — ECDSA라서 어렵다
이더리움 EOA는 secp256k1 ECDSA를 쓰는데, 서명 값이 s = k⁻¹(z + r·d) 형태로 개인키 d와 난수 nonce k가 곱셈으로 얽혀 있다. 지분을 단순히 더해서는 이 곱셈 항을 분산 계산할 수 없다(덧셈 구조인 Schnorr/EdDSA는 threshold화가 상대적으로 쉽다). 그래서 threshold ECDSA는 별도의 정교한 프로토콜 계보를 갖게 됐다.
| 프로토콜 | 핵심 기법 | 상태 |
|---|---|---|
| Lindell 2017 | 2-party, Paillier | 2자 전용 |
| GG18 / GG20 | Paillier MtA | 실전 취약점 발견 |
| CGGMP21 | ZK 범위증명 보강 | 알려진 취약점 없음 |
| DKLs19/23 | Paillier 대신 OT(Oblivious Transfer) | 라운드 적음, 활발 채택 |
무서운 건 이 선택이 곧 보안 결정이라는 점이다. GG18/GG20은 하위 프로토콜(MtA) 파라미터에 대한 영지식 범위증명이 빠져 있었고, 그 틈으로 악의적 파티가 조작된 파라미터를 주입해 소수의 서명(약 16회)만으로 상대 지분을 추출, 전체 키를 복원할 수 있었다(BitForge, CVE-2023-33241). 다수의 지갑 벤더가 영향을 받았다. 논문이 있는 프로토콜이라고 안전한 게 아니라, 어느 계열을 고르느냐부터가 보안이다.
개념 4 — 멀티시그와는 레이어가 다르다
MPC-TSS는 흔히 온체인 멀티시그(Safe)나 ERC-4337 스마트 계정과 대립 구도로 소개되는데, 정리해보니 레이어가 다르다. 멀티시그와 스마트 계정은 온체인 계정 형태고, MPC-TSS는 키 관리 방식이다. 실제로 “스마트 계정의 서명 키를 MPC로 관리”처럼 결합해서 쓰기도 한다.
| 항목 | 온체인 멀티시그 | ERC-4337 스마트 계정 | MPC-TSS |
|---|---|---|---|
| 키 저장 | 완전한 키 N개 | 계정 로직이 정한 키 | 완전한 키 없음, 지분 |
| 검증 위치 | 온체인 컨트랙트 | 온체인(EntryPoint) | 오프체인 결합 |
| 온체인 비용 | 높음 | 중~높음 | EOA와 동일 |
| 정책 표현 | 온체인(투명·감사 가능) | 온체인(프로그래머블) | 오프체인(불투명) |
마지막 줄이 양날의 검이다. MPC의 임계값·승인 정책은 온체인에 드러나지 않는다. 2-of-3이든 3-of-5든 체인에는 평범한 EOA 서명 하나로 보인다. 가스비 관점에선 장점이지만, “누가 승인했는지를 온체인으로 증명해야 하는” 거버넌스 용도에는 부적합하다는 뜻이기도 하다. 그 용도는 멀티시그의 영역이다.
현실 — 감사된 순수 TS 구현은 없다
라이브러리 생태계를 보면 도입 난이도가 체감된다. 범용 t-of-n threshold ECDSA를 프로덕션 수준의 순수 JS/TS로 제공하는 오픈소스는 사실상 없다. 쓸 만한 코어는 Go(Binance tss-lib)나 Rust(DKLs 계열)이고, 브라우저에서 쓰려면 WASM 빌드·번들·보안 검증을 직접 떠안아야 한다. 그래서 실무 도입은 대부분 Web3Auth, Fireblocks, Silence Laboratories 같은 외부 제공자(WaaS, Wallet-as-a-Service)를 끼는 형태가 되고, 네트워크·과금·벤더 종속이 전제가 된다.
자체 구현은 선택지에서 뺐다. 암호학(Paillier/OT, 영지식 증명)과 분산 시스템(라운드 통신, abort 처리)의 결합 영역이라, 미묘한 실수 하나가 곧 키 유출이다. BitForge가 그 증거다.
대신 발견한 것이 있다. SDK에 이미 있던 SignerFn 추상화다.
// src/core/signer/create-signer-fn.ts
export type SignerFn = (data: string | Uint8Array) => Promise<string>;
키를 노출하지 않고 서명 기능만 제공하는 비동기 함수 — 로컬 ECDSA든 원격 MPC든 이 시그니처로 표현된다. 서명 백엔드를 갈아 끼울 접합점(seam)이 이미 있었던 셈이다. MPC 도입이 확정되면 외부 제공자를 이 계약으로 감싸는 어댑터만 추가하면 되고, 소비자 코드는 바뀌지 않는다.
회고
MPC-TSS는 키를 더 잘 숨기는 기술이 아니라, “완전한 키가 존재하는 순간” 자체를 없애는 기술이다. 대신 서명이 혼자 하는 연산에서 여럿이 하는 협력으로 바뀐다 — 공짜 점심은 없다.
개념을 정리하고 나니 오히려 명확해진 건 “지금 당장 쓸 기술은 아니다”라는 판단이었다. 하지만 그 판단을 하려면 Shamir와의 차이, ECDSA의 난점, 프로토콜 계보, 생태계의 현실 정도는 알고 있어야 했다. 리서치의 절반은 도입하지 않을 이유를 정확히 말할 수 있게 되는 일이다.