v0.5
EVM Safe Multisig 기반 2단계 서명 구현 설계
1. 설계 전제
1.1. 아키텍처 선정 결과
option-evaluation.md에서 옵션 A(오프체인 승인 + 온체인 Safe execTransaction 실행 분리) 가 최적안으로 선정되었다. EVM 구현은 이 결정을 전제로 한다.
개인 기기 = ECDSA secp256k1 오프체인 승인만 수행. EVM 온체인 서명(Safe owner 서명)에 참여하지 않는다.
콜드월렛 = ECDSA secp256k1 Safe owner 서명 전용. ApprovalBundle 검증 통과 후에만 서명을 실행한다.
키 독립성: approval_sk와 master_sk는 수학적으로 완전 독립. 동일한 secp256k1 곡선을 사용하나 독립 시드에서 파생.
1.2. Bybit/Safe 해킹 교훈 반영
2025년 2월 Bybit 해킹 사건에서 Safe 프론트엔드 공급망 공격을 통해 delegatecall 조작이 발생했다. 이 교훈을 EVM 2단계 서명 설계 전반에 반영한다.
교훈
적용
프론트엔드 변조로 서명 대상 조작 가능
개인 기기 WYSIWYS + 콜드월렛 WYSIWYS 이중 확인
delegatecall (operation=1)로 컨트랙트 로직 교체
WYSIWYS에서 operation 필드 경고 표시
블라인드 서명으로 사용자가 내용 확인 불가
EIP-712 구조화 데이터 human-readable 파싱 필수
서명 인터페이스 침해
SE 하드웨어 레벨 WYSIWYS가 소프트웨어 침해와 독립
2. EVM 온체인 서명 구조
2.1. Safe Multisig 컨트랙트 구성
운용 모드
Safe 설정
설명
단일 콜드월렛
Safe 1-of-1 owner
콜드월렛 주소 1개가 유일한 owner. Approval Verifier 통과 후 단일 서명
복수 콜드월렛
Safe M-of-N owner
콜드월렛 주소 N개를 owner로 등록. M개 서명으로 execTransaction 실행
Safe Multisig Contract
├── owners: [coldwallet_addr_1, coldwallet_addr_2, ..., coldwallet_addr_N]
├── threshold: M
├── nonce: auto-increment
└── execTransaction(to, value, data, operation, safeTxGas, baseGas, gasPrice,
gasToken, refundReceiver, signatures)
2.2. Safe owner 서명 구조
Safe TX 서명 순서: 주소 오름차순 정렬 (Safe 프로토콜 요구사항)
signatures = sort_by_address(
[ECDSA_Sign(master_sk_1, safeTxHash),
ECDSA_Sign(master_sk_2, safeTxHash),
...,
ECDSA_Sign(master_sk_M, safeTxHash)]
)
각 서명: 65바이트 (r: 32, s: 32, v: 1)
signatures 연결: M * 65 바이트
2.3. Safe TX Hash 계산
safeTxHash = keccak256(
bytes1(0x19) ||
bytes1(0x01) ||
domainSeparator ||
safeTxStructHash
)
domainSeparator = keccak256(
abi.encode(
DOMAIN_SEPARATOR_TYPEHASH,
chainId,
safeAddress
)
)
safeTxStructHash = keccak256(
abi.encode(
SAFE_TX_TYPEHASH,
to, value, keccak256(data), operation,
safeTxGas, baseGas, gasPrice,
gasToken, refundReceiver, nonce
)
)
3. 오프체인 승인 -> 온체인 Safe 실행 연결 흐름
3.1. 5단계 파이프라인 EVM 특화
(1) REQUEST (2) APPROVE (3) COLLECT
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ 대시보드 │ │ 개인 기기 x M │ │ 오프라인 앱 │
│ │ │ │ │ │
│ Safe TX 생성 │ │ safeTxHash에 │ │ M개 승인 서명 │
│ + safeTxHash │ │ EIP-712 승인 │ │ 수집 → Bundle │
│ 계산 │ │ 서명 생성 │ │ │
└───────┬──────┘ └───────┬───────┘ └───────┬──────┘
│ │ │
[TB-3 QR] [TB-7 QR/NFC] [TB-4 QR/NFC]
│ │ │
▼ ▼ ▼
(4) SIGN (5) BROADCAST
┌──────────────┐ ┌──────────────┐
│ 콜드월렛 SE │ │ 대시보드 │
│ │ │ │
│ ApprovalBundle│ │ execTransaction│
│ 검증 → ECDSA │ │ 호출 │
│ Safe owner │───────────────────────────▶│ (relayer 또는 │
│ 서명 │ [TB-4 → TB-3] │ 직접 전송) │
└──────────────┘ └──────────────┘
3.2. 단계별 상세
(1) Request: Safe TX 생성
항목
상세
실행 주체
Initiator (대시보드, Zone 1)
입력
출금 요청 (수신 주소, 금액, 토큰 컨트랙트, 호출 데이터)
출력
Safe TX 파라미터 + safeTxHash
Safe TX 필드
to: 수신 주소, value: ETH 금액 (wei), data: 호출 데이터 (ERC-20 transfer 등), operation: 0 (call) 또는 1 (delegatecall, 경고), safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce
safeTxHash 계산
EIP-712 도메인 분리자 + Safe TX 구조체 해시 (섹션 2.3 참조)
QR 인코딩
UR type: dcent-approval-request (45050), chain_type: 0x01 (EVM)
(2) Approve: M-of-N 개인 기기 EIP-712 승인
항목
상세
실행 주체
Approver x M (개인 기기, Zone 3+)
입력
ApprovalRequest (safeTxHash + 표시용 메타데이터)
출력
ApprovalResponse (EIP-712 구조화 승인 서명)
서명 방식
EIP-712 typed data 서명: 승인 전용 도메인 + safeTxHash 바인딩
EIP-712 도메인
{name: "D'CENT Approval", version: "1", chainId: target_chain_id, verifyingContract: safe_address}
승인 구조체
Approval {safeTxHash: bytes32, approver: address, timestamp: uint256, nonce: uint256}
서명
sig = ECDSA_Sign(approval_sk, keccak256(EIP-712_encode(domain, approval_struct)))
WYSIWYS 표시
수신 주소, 전송 금액 (ETH/토큰 단위 변환), function selector 해석, operation 경고
비동기
각 Approver 독립 서명 (stateless)
전달 채널
TB-7: QR (UR type: dcent-approval-response 45051) 또는 NFC APDU (INS 0x51)
(3) Collect: ApprovalBundle 생성
항목
상세
실행 주체
Operator (오프라인 앱, Zone 2)
입력
M개 ApprovalResponse
출력
ApprovalBundle (CBOR, chain_id 포함)
처리
BTC와 동일: M개 수집 → 형식 검증 → Bundle 구성
전달
TB-4: QR (UR type: dcent-approval-bundle 45052) 또는 NFC APDU
(4) Sign: 콜드월렛 SE 검증 및 Safe owner 서명
항목
상세
실행 주체
Operator → 콜드월렛 SE (Zone 3)
입력
ApprovalBundle + Safe TX 파라미터
출력
Safe owner 서명 (ECDSA, 65바이트)
SE 내부 처리
BTC Approval Verifier와 동일한 7단계 + chain_id 확인 추가
추가 검증
(1) chain_id 일치 확인 (replay 방어), (2) safeTxHash 독립 재계산으로 변조 방지, (3) operation=1 (delegatecall) 시 추가 경고
서명
sig = ECDSA_Sign(master_sk, safeTxHash) — 65바이트 (r, s, v)
WYSIWYS 표시
수신 주소, 금액, 체인 이름, function 요약, operation 경고, 승인 현황
(5) Broadcast: Safe execTransaction 호출
항목
상세
실행 주체
System (대시보드, Zone 1)
입력
Safe owner 서명 (M개, 복수 콜드월렛 시)
출력
Transaction Hash
호출
safe.execTransaction(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signatures)
전송 방식
(1) 대시보드 직접 전송 (EOA가 가스 지불) 또는 (2) Relayer 서비스 (가스 추상화)
모니터링
TX receipt 확인 → success 상태 확인 → 이벤트 로그 검증
4. 오프체인 승인 vs 온체인 Safe 서명 분리 설계
4.1. 분리 구조 (ARCH-03 핵심)
┌─────────────────────────────────────────────────────────┐
│ │
│ 오프체인 승인 (Off-chain) │
│ ┌─────────────────────────────────────────┐ │
│ │ 개인 기기 EIP-712 서명 │ │
│ │ - 블록체인에 기록되지 않음 │ │
│ │ - 가스비 없음 │ │
│ │ - 비동기 수집 가능 │ │
│ │ - 개인 기기 키 온체인 노출 없음 │ │
│ │ - 교체 시 블록체인 영향 없음 │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ (ApprovalBundle via 에어갭) │
│ │
│ 온체인 서명 (On-chain) │
│ ┌─────────────────────────────────────────┐ │
│ │ 콜드월렛 ECDSA Safe owner 서명 │ │
│ │ - execTransaction에 포함되어 블록체인 기록 │ │
│ │ - 가스비 발생 │ │
│ │ - 콜드월렛 주소가 Safe owner로 공개 │ │
│ │ - 변경 시 Safe owner 교체 필요 │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
4.2. 분리의 보안 이점
이점
설명
개인 기기 키 비노출
오프체인 승인 서명은 블록체인에 기록되지 않으므로, 공격자가 온체인 데이터에서 개인 기기 공개 키를 역추적할 수 없다
승인 과정 무비용
승인 서명 수집에 가스비가 발생하지 않으므로, 다수 Approver의 비동기 승인이 경제적으로 부담 없다
키 교체 비용 제로
개인 기기 키 교체 시 온체인 Safe 설정 변경이 불필요하며, 콜드월렛 SE의 공개키 레지스트리만 업데이트하면 된다
승인 프라이버시
승인자 목록과 승인 패턴이 블록체인에 노출되지 않아 조직 내부 구조 유추가 불가능하다
Bybit 방어
오프체인 승인 단계에서 WYSIWYS로 TX 내용을 확인한 후, 온체인 서명 단계에서 콜드월렛 WYSIWYS로 이중 확인한다. 두 단계 모두 SE 하드웨어 기반이므로 소프트웨어 프론트엔드 변조에 독립적이다
4.3. Bybit 공격 패턴 방어 상세
Bybit 공격 단계
D'CENT Enterprise 방어
1. Safe 프론트엔드 공급망 공격
대시보드 프론트엔드가 변조되어도, 개인 기기 WYSIWYS + 콜드월렛 WYSIWYS에서 TX 내용 독립 확인
2. delegatecall (operation=1) 삽입
개인 기기/콜드월렛 WYSIWYS에서 operation=1 감지 시 경고 표시 + 고액 TX 블라인드 서명 차단
3. 서명 대상 데이터 변조
콜드월렛 SE가 safeTxHash를 독립 재계산하여 ApprovalBundle.tx_hash와 비교. 불일치 시 거부
4. 블라인드 서명 유도
EIP-712 구조화 데이터를 human-readable로 파싱하여 표시. 미지 컨트랙트는 raw data 경고
5. ApprovalBundle EVM 스키마 (CBOR)
5.1. CDDL 정의 (BTC와 공통 구조)
; ApprovalBundle - BTC/EVM 공통 구조
; UR type: dcent-approval-bundle (CBOR 태그 45052)
; EVM은 chain_id 필드가 반드시 포함됨
approval-bundle-evm = {
1 : bytes .size 32 , ; tx_hash: EIP-712 safeTxHash
2 : [ + approval-signature ], ; approval_signatures: M개 EIP-712 승인 서명
3 : text , ; policy_id: 적용된 정책 ID
4 : quorum , ; quorum: 정족수 정보
5 : uint , ; chain_id: EVM 체인 ID (필수)
6 : uint , ; timestamp: Bundle 생성 시간
7 : bytes .size 16 , ; request_id: 승인 요청 고유 ID
}
approval-signature = {
1 : bytes .size 33 , ; pubkey: 승인자 공개 키 (compressed)
2 : bytes .size 65 , ; sig: EIP-712 서명 (r: 32 + s: 32 + v: 1)
3 : text , ; device_id: 개인 기기 식별자
4 : uint , ; timestamp: 서명 생성 시간
}
quorum = {
1 : uint , ; required: M
2 : uint , ; collected: >= M
}
5.2. BTC/EVM 공통 구조 유지 원칙
항목
BTC
EVM
공통
tx_hash
BIP-341 sighash
EIP-712 safeTxHash
32바이트 해시
chain_id
생략
필수 (1=Ethereum, 137=Polygon 등)
chain_id 유무로 구분
sig 크기
64바이트 (r||s compact)
65바이트 (r||s||v)
1바이트 차이
approval-signature 구조
동일
동일
완전 공통
UR type
동일 (45052)
동일 (45052)
단일 타입
5.3. 예시 (CBOR 진단 표기)
45052({
1: h'b2c3d4...32bytes...safeTxHash', ; tx_hash
2: [ ; approval_signatures
{
1: h'02abc...33bytes...pubkey', ; pubkey
2: h'3045...65bytes...eip712sig', ; sig (r||s||v)
3: "dcent-bio-001", ; device_id
4: 1774745600, ; timestamp
},
{
1: h'03def...33bytes...pubkey',
2: h'3046...65bytes...eip712sig',
3: "dcent-bio-002",
4: 1774745620,
},
],
3: "policy-daily-limit-v1", ; policy_id
4: { 1: 2, 2: 2 }, ; quorum: 2-of-3
5: 1, ; chain_id: Ethereum Mainnet
6: 1774745650, ; bundle timestamp
7: h'aabbccdd...16bytes...request_id', ; request_id
})
6. EIP-712 구조화 데이터 WYSIWYS 파싱
6.1. 파싱 계층
┌──────────────────────────────────────────────────────────┐
│ EIP-712 WYSIWYS 파싱 엔진 (개인 기기 + 콜드월렛) │
│ │
│ Layer 1: 기본 필드 파싱 (모든 TX) │
│ - to: 수신 주소 (전체 표시, 체크섬 포함) │
│ - value: 전송 금액 (wei → ETH 변환, 소수점 표시) │
│ - operation: 0=call (정상), 1=delegatecall (경고) │
│ - chainId: 체인 이름 매핑 (1=Ethereum, 137=Polygon) │
│ │
│ Layer 2: 알려진 컨트랙트 파싱 │
│ - ERC-20 transfer(address,uint256): "전송 X TOKEN to Y" │
│ - ERC-20 approve(address,uint256): "승인 X TOKEN for Y" │
│ - ERC-721 transferFrom: "NFT #ID 전송" │
│ - WETH deposit/withdraw: "ETH → WETH 변환" │
│ │
│ Layer 3: 미지 컨트랙트 처리 │
│ - Function selector (첫 4바이트) 표시 │
│ - Raw data 길이 표시 │
│ - 고액 TX: 블라인드 서명 차단 + "확인 불가 TX" 경고 │
│ - 저액 TX: 경고 표시 + 사용자 확인 후 허용 │
└──────────────────────────────────────────────────────────┘
6.2. WYSIWYS 화면 표시 항목
항목
표시 형식
예시
수신 주소
0x + 체크섬 주소 (40자 전체)
0xdAC17F958D2ee523a2206206994597C13D831ec7
전송 금액
숫자 + 단위 (ETH/토큰 심볼)
1.5 ETH 또는 1,000 USDT
Function
알려진 경우: 한글 설명 / 미지: selector hex
ERC-20 전송 또는 0xa9059cbb
Operation
call (정상) 또는 DELEGATECALL 경고
DELEGATECALL 위험
체인
체인 이름 + chain ID
Ethereum (1)
수수료
가스 x 가스 단가 (ETH 변환)
~0.003 ETH
승인 현황
M/N 진행 상태
승인: 2/3
정책
적용된 정책 요약
일일 한도: 50 ETH 중 48.5 ETH 사용
6.3. 블라인드 서명 방어 정책
조건
동작
근거
알려진 컨트랙트 + 파싱 성공
정상 표시 → 승인 허용
사용자가 TX 내용을 완전히 이해
미지 컨트랙트 + 저액 (< 일일한도 10%)
경고 표시 + 확인 후 허용
저위험이나 사용자 주의 환기
미지 컨트랙트 + 고액 (>= 일일한도 10%)
차단 + "확인 불가 TX" 메시지
Bybit 패턴 방어 (Pitfall 4)
operation=1 (delegatecall)
강력 경고 + 고액 시 차단
delegatecall은 컨트랙트 로직 교체 가능
data 길이 > 10KB
경고 + 전체 데이터 표시 불가 고지
대량 데이터 TX의 위험성
7. 콜드월렛 SE Approval Verifier EVM 로직
7.1. BTC 대비 추가/변경 사항
검증 항목
BTC
EVM
변경 사항
tx_hash 검증
BIP-341 sighash
safeTxHash
계산 방식 다름: EIP-712 도메인+구조체 해시
chain_id 확인
해당 없음
필수
추가: ApprovalBundle.chain_id와 SE 설정 chain_id 일치 확인
safeTxHash 재계산
해당 없음
필수
추가: SE가 Safe TX 파라미터로 safeTxHash를 독립 재계산하여 변조 방지
서명 알고리즘
BIP-340 Schnorr
ECDSA secp256k1
변경: 서명 함수 및 출력 형식
정족수 검증
동일
동일
변경 없음
정책 엔진
동일
동일
변경 없음
WYSIWYS
동일 구조
EIP-712 파싱 추가
추가: function selector 해석, operation 경고
7.2. EVM 전용 검증 흐름
┌─────────────────────────────────────────────────────────┐
│ Approval Verifier EVM 모드 (콜드월렛 SE 내부) │
│ │
│ Step 1-4: BTC와 동일 (Bundle 디코딩, 서명 검증, 정족수) │
│ │
│ Step 4a: chain_id 일치 검증 [EVM 추가] │
│ - ApprovalBundle.chain_id == SE 설정 chain_id │
│ - 불일치 시: REJECT (ERROR_CHAIN_ID_MISMATCH) │
│ │
│ Step 4b: safeTxHash 독립 재계산 [EVM 추가] │
│ - Safe TX 파라미터(to, value, data, operation, ...)로 │
│ safeTxHash를 SE 내부에서 독립 계산 │
│ - 계산된 hash == ApprovalBundle.tx_hash │
│ - 불일치 시: REJECT (ERROR_TX_HASH_MISMATCH) │
│ │
│ Step 5: HW 정책 엔진 4개 불변 규칙 검증 │
│ │
│ Step 6: WYSIWYS + Operator 물리 확인 │
│ - EIP-712 파싱 엔진으로 human-readable 표시 │
│ - operation=1 경고 표시 │
│ │
│ Step 7: ECDSA secp256k1 Safe owner 서명 실행 │
│ sig = ECDSA_Sign(master_sk, safeTxHash) │
│ 감사 로그 기록 │
│ │
│ 출력: Safe owner 서명 (65바이트) + 감사 로그 │
└─────────────────────────────────────────────────────────┘
7.3. EVM 전용 에러 코드
코드
이름
설명
0x0A
ERROR_CHAIN_ID_MISMATCH
ApprovalBundle chain_id와 SE 설정 chain_id 불일치
0x0B
ERROR_DELEGATECALL_BLOCKED
operation=1 (delegatecall) 고액 TX 차단
0x0C
ERROR_BLIND_SIGNING_BLOCKED
미지 컨트랙트 고액 TX 블라인드 서명 차단
(기존 BTC 에러 코드 0x01~0x09는 공유)
8. 복수 콜드월렛 운용 시 Safe M-of-N 처리
8.1. 복수 콜드월렛 서명 흐름
복수 콜드월렛 운용 시 각 콜드월렛이 개별적으로 Safe owner 서명을 생성하고, 오프라인 앱이 M개 서명을 수집하여 execTransaction을 호출한다.
콜드월렛 1 (Zone 3): ApprovalBundle 검증 → owner_sig_1
콜드월렛 2 (Zone 3): ApprovalBundle 검증 → owner_sig_2
...
콜드월렛 M (Zone 3): ApprovalBundle 검증 → owner_sig_M
오프라인 앱 (Zone 2): M개 owner_sig 수집 → 주소 오름차순 정렬 → signatures 연결
대시보드 (Zone 1): execTransaction(... , signatures) 호출
8.2. 서명 순서 정렬
Safe 프로토콜은 signatures 배열에서 서명자 주소가 오름차순으로 정렬되어야 한다. 오프라인 앱이 수집 후 정렬을 수행한다.
signatures = sort(owner_sigs, key=lambda s: address_from_pubkey(s.pubkey))
packed = concat([sig.r + sig.s + sig.v for sig in signatures])
9. 지원 EVM 체인 및 체인별 차이
체인
chain_id
Safe 컨트랙트
특이사항
Ethereum Mainnet
1
Safe (공식)
기본 지원
Polygon
137
Safe (공식)
가스비 저렴, 동일 프로토콜
Arbitrum
42161
Safe (공식)
L2 가스 구조 차이
Optimism
10
Safe (공식)
L2 가스 구조 차이
Base
8453
Safe (공식)
L2
Avalanche C-Chain
43114
Safe (공식)
동일 프로토콜
BNB Smart Chain
56
Safe (공식)
동일 프로토콜
모든 EVM 호환 체인에서 동일한 Safe Multisig + ApprovalBundle 구조를 사용한다. chain_id 필드만 변경하면 다중 체인을 지원할 수 있다.
설계 기준: option-evaluation.md 옵션 A 채택 결과
참조: .planning/research/ARCHITECTURE.md 섹션 3.2
EVM 프로토콜: EIP-712 (Typed Structured Data), Safe Multisig (execTransaction), ERC-20/721
보안 참조: Bybit/Safe 해킹 사후 분석 (Ledger, NCC Group)
관련 문서