v0.0
D'CENT X 펌웨어 서명 인터페이스 설계서¶
1. Executive Summary¶
본 문서는 D'CENT X 콜드월렛의 Secure Element(SE) 내부 앱릿 구조와 외부 통신 인터페이스를 설계한다. SignRequest/SignResponse 메시지의 SE 내부 처리 흐름, PolicyUpdate 인터페이스, WhitelistVerify 인터페이스, SE 상태 조회 인터페이스를 명세하여 펌웨어 개발팀이 구현에 착수할 수 있는 수준의 기술 명세를 제공한다.
설계 범위: - SE 앱릿 구조 (Signing, Policy, WYSIWYS) - SignRequest/SignResponse 메시지 처리 흐름 - PolicyUpdate 및 WhitelistVerify 인터페이스 - SE 상태 조회 인터페이스 - 에러 핸들링 및 복구 절차
2. SE 통신 인터페이스 개요¶
2.1. SE 앱릿 구조¶
┌──────────────────────────────────────────────────────┐
│ D'CENT X Secure Element │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Enterprise Applet Container │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Signing │ │ WYSIWYS │ │ │
│ │ │ Applet │ │ Applet │ │ │
│ │ │ │ │ │ │ │
│ │ │ ● ECDSA Sign │ │ ● PSBT Parse │ │ │
│ │ │ ● Schnorr │ │ ● RLP/ABI │ │ │
│ │ │ ● Key Derive │ │ ● Hash Gen │ │ │
│ │ │ ● Nonce Gen │ │ ● Display │ │ │
│ │ │ (MuSig2) │ │ ● Warning │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │
│ │ │ Internal API │ │ │
│ │ │ │ │ │
│ │ ┌──────▼──────────────────▼──────┐ │ │
│ │ │ Policy Applet │ │ │
│ │ │ │ │ │
│ │ │ ● Amount Limit Check │ │ │
│ │ │ ● Merkle Root Verify │ │ │
│ │ │ ● Daily Counter │ │ │
│ │ │ ● Rate Limit │ │ │
│ │ │ ● Cooldown Timer │ │ │
│ │ │ ● Admin Sig Verify │ │ │
│ │ └────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Secure Storage │ │ │
│ │ │ │ │ │
│ │ │ ● Private Keys (HD derived) │ │ │
│ │ │ ● Policy Data (256B) │ │ │
│ │ │ ● Merkle Root (32B) │ │ │
│ │ │ ● Admin Public Keys (512B) │ │ │
│ │ │ ● Sign Counter (4B) │ │ │
│ │ │ ● Token DB (2KB) + Selector DB (4KB) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ External Interface │ │
│ │ QR (카메라 입력 / 화면 출력) │ │
│ │ USB-C (HID) │ │
│ │ 물리 버튼 (승인 / 거부) │ │
│ │ 디스플레이 (WYSIWYS 표시) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
2.2. 앱릿 간 통신 (Internal API)¶
SignRequest 수신
│
▼
[WYSIWYS Applet]
│ parse(raw_tx) → ParsedTransaction
│ generateHash(parsed) → SE_PARSE_HASH
│ compareHash(SE_PARSE_HASH, APP_HASH) → match/mismatch
│ displayTransaction(parsed) → 화면 표시
│
▼ (일치 + 사용자 물리 버튼 승인)
[Policy Applet]
│ checkAmountLimit(tx.amount) → pass/reject
│ verifyWhitelist(recipient, merkle_proof) → pass/reject
│ checkDailyLimit(tx.amount) → pass/reject
│ checkRateLimit() → pass/reject
│
▼ (전체 통과)
[Signing Applet]
│ deriveKey(derivation_path) → private_key
│ sign(data_to_sign, private_key) → signature
│ incrementCounter()
│
▼
SignResponse 생성 + 반환
3. SignRequest 메시지 구조¶
3.1. 공통 헤더¶
SignRequestHeader {
version: uint8 // 프로토콜 버전 (0x01)
request_id: bytes(16) // UUID v4
chain_type: uint8 // 0x00=BTC, 0x01=EVM, 0x02=SOL(향후)
timestamp_ref: uint32 // 참조용 타임스탬프 (SE 미신뢰)
payload_hash: bytes(32) // SHA-256 페이로드 해시
}
3.2. BTC SignRequest¶
BTCSignRequest {
header: SignRequestHeader
psbt: bytes // PSBT 바이너리 (BIP-174/370)
derivation_path: bytes // BIP-32 경로 (바이너리: 4B × depth)
signer_index: uint8 // MuSig2 참여자 인덱스
wallet_id: bytes(16) // 월렛 식별자
policy_version: uint32 // 정책 버전 (SE 저장값과 대조)
whitelist_version: uint32 // 화이트리스트 버전
app_hash: bytes(32) // WYSIWYS 검증용 해시
musig2_data: { // MuSig2 라운드 데이터
round: uint8 // 1=nonce, 2=partial_sig
aggregated_nonce: bytes(66)? // Round 2
participant_pubkeys: [bytes(33)]
}?
whitelist_proof: {
recipient: bytes // 수신 주소
proof: [bytes(32)] // Merkle Proof
}?
}
3.3. EVM SignRequest¶
EVMSignRequest {
header: SignRequestHeader
chain_id: uint64 // EIP-155 Chain ID
rlp_tx: bytes // RLP 인코딩된 미서명 TX
derivation_path: bytes
wallet_id: bytes(16)
policy_version: uint32
whitelist_version: uint32
app_hash: bytes(32)
eip712_domain: bytes(32)? // EIP-712 도메인 세퍼레이터
eip712_hash: bytes(32)? // EIP-712 메시지 해시
whitelist_proof: {...}?
}
3.4. SE 내부 처리 흐름¶
[1] SignRequest CBOR 수신 및 디코딩
│
▼
[2] 헤더 검증
├─ version 지원 확인
├─ payload_hash 무결성 확인
└─ policy_version/whitelist_version 대조
├─ 불일치 → POLICY_VERSION_MISMATCH (0x24) 반환
└─ 일치 → 계속
│
▼
[3] WYSIWYS Applet 호출
├─ raw_tx 독립 파싱 (PSBT 디코딩 또는 RLP+ABI 디코딩)
├─ ParsedTransaction 생성
├─ SE_PARSE_HASH = SHA-256(recipients || amounts || fee || chain)
├─ APP_HASH 대조
│ ├─ 불일치 → WYSIWYS_HASH_MISMATCH (0x30) 반환 + 잠금 카운터 증가
│ │ 3회 연속 → WYSIWYS_LOCKOUT (0x32)
│ └─ 일치 → 계속
├─ 화면에 TX 내용 표시 (수신 주소, 금액, 수수료, 체인)
└─ 물리 버튼 대기
├─ 거부 → REJECTED_BY_USER (0x10) 반환
├─ 타임아웃(60초) → CANCELLED_BY_TIMEOUT (0x11) 반환
└─ 승인 → 계속
│
▼
[4] Policy Applet 호출
├─ 화이트리스트 검증 (Merkle Proof → Root 대조)
│ └─ 실패 → POLICY_WHITELIST_FAILED (0x21)
├─ 건당 한도 검증 (tx.amount vs max_per_tx)
│ └─ 초과 → POLICY_AMOUNT_EXCEEDED (0x20)
├─ 일일 누적 검증 (daily_spent + tx.amount vs daily_max)
│ └─ 초과 → POLICY_DAILY_LIMIT_EXCEEDED (0x22)
└─ 서명 횟수 검증 (window_signs vs max_signs)
└─ 초과 → POLICY_RATE_LIMIT_EXCEEDED (0x23)
│
▼
[5] Signing Applet 호출
├─ BTC: deriveKey(path) → Schnorr sign(sighash)
│ ├─ MuSig2 Round 1: generateNonce() → public_nonce 반환
│ └─ MuSig2 Round 2: partialSign(sighash, agg_nonce) → partial_sig 반환
├─ EVM: deriveKey(path) → ECDSA sign(tx_hash)
├─ 서명 카운터 증가
└─ 일일 누적 금액 업데이트
│
▼
[6] SignResponse 생성 + 반환
4. SignResponse 메시지 구조¶
4.1. 성공 응답¶
SignResponse_Success {
header: {
version: uint8
request_id: bytes(16) // 원본 요청 ID
message_type: 0x03
}
status_code: 0x00 // SUCCESS
signature: {
chain_type: uint8
sig_bytes: bytes // BTC Schnorr: 64B, EVM ECDSA: 65B (r,s,v)
pubkey: bytes(33) // 서명한 공개키
derivation_path: bytes
}
se_parse_hash: bytes(32) // WYSIWYS 검증 해시
sign_counter: uint32 // 현재 서명 카운터
musig2_data: { // MuSig2 Round 1 응답 (해당 시)
public_nonce: bytes(66)
}?
}
4.2. 실패 응답¶
SignResponse_Failure {
header: {...}
status_code: uint8 // 0x10-0xFF
se_parse_hash: bytes(32) // 파싱 성공 시에만 포함
sign_counter: uint32
error_detail: {
error_code: uint8
error_message: string?
policy_info: { // 정책 위반 시
violated_policy: uint8
current_value: bytes(32)
requested_value: bytes(32)
}?
}
}
4.3. 상태 코드 체계¶
(component-data-flow.md 섹션 5.2의 상태 코드 체계를 SE 내부에서 그대로 사용)
| 범위 | 카테고리 | 설명 |
|---|---|---|
| 0x00 | 성공 | 서명 완료 |
| 0x10-0x1F | 사용자 거부 | 물리 버튼 거부, 타임아웃 |
| 0x20-0x2F | 정책 위반 | HW 정책 엔진 거부 (6종 코드) |
| 0x30-0x3F | WYSIWYS | 해시 불일치, 파싱 실패, 잠금 |
| 0x40-0x4F | 파싱 에러 | 데이터 파싱 불가, 미지원 체인 |
| 0xF0-0xFF | 시스템 | 내부 오류, 미초기화 |
5. PolicyUpdate 인터페이스¶
5.1. PolicyUpdate 처리 흐름¶
[1] PolicyUpdate CBOR 수신
│
▼
[2] 기본 검증
├─ version == current_version + 1 (롤백 방지)
│ └─ 불일치 → POLICY_VERSION_MISMATCH
├─ previous_value == SE 현재 저장값
│ └─ 불일치 → 거부 (데이터 무결성)
└─ 쿨다운 상태 확인
└─ 쿨다운 중 → POLICY_COOLDOWN_ACTIVE (0x25)
│
▼
[3] Admin 쿼럼 서명 검증
├─ admin_signatures 각각의 pubkey가 SE 저장 Admin 목록에 포함
├─ 각 서명의 ECDSA 검증 (payload_hash 기준)
└─ 유효 서명 수 >= quorum_threshold
└─ 미달 → POLICY_SIGNATURE_INVALID (0x06)
│
▼
[4] 화면 표시
├─ "정책 변경: [항목] [이전값] → [신규값]"
└─ 물리 버튼 2회 확인
├─ 1회: "정책 변경을 인지합니까?"
└─ 2회: "변경을 최종 승인합니까?"
│
▼
[5] 쿨다운 적용
├─ 보안 강화 방향(한도 감소): 즉시 적용
└─ 보안 약화 방향(한도 증가): 쿨다운 시작
└─ cooldown_remaining = cooldown_seconds
└─ 쿨다운 완료 시 신규 값 활성화
│
▼
[6] PolicyResponse 반환
├─ status: SUCCESS / PENDING_COOLDOWN
├─ new_version: uint32
└─ cooldown_remaining: uint32
5.2. Admin 공개키 관리¶
SE에 저장되는 Admin 공개키 세트:
admin_keys: [bytes(33)] // 최대 8개
admin_threshold: uint8 // Admin 쿼럼 임계값
Admin 키 세트 변경:
- 기존 Admin 전체 서명 필수 (N-of-N)
- 키 세레모니 수준의 절차 요구
- 변경 후 72시간 쿨다운
6. WhitelistVerify 인터페이스¶
6.1. Merkle Proof 검증 흐름¶
[1] WhitelistVerifyRequest 수신
├─ recipient: 수신 주소
├─ proof: [bytes(32)] (Merkle Proof, 최대 20 depth)
└─ whitelist_version: uint32
│
▼
[2] 버전 확인
└─ whitelist_version == SE 저장 버전
└─ 불일치 → 거부 (동기화 필요)
│
▼
[3] Merkle 검증
├─ leaf = SHA-256(recipient)
├─ for each sibling in proof:
│ current = SHA-256(min(current, sibling) || max(current, sibling))
└─ computed_root == SE_stored_merkle_root ?
├─ 일치 → PASS
└─ 불일치 → REJECT (POLICY_WHITELIST_FAILED)
│
▼
[4] 결과 반환 (1 byte: 0x00=통과, 0x01=실패)
6.2. 성능¶
| 항목 | 수치 |
|---|---|
| Proof depth 20 | 1,048,576 주소 지원 |
| SHA-256 × 20 | ~50ms |
| SE 저장: Merkle Root | 32 bytes 고정 |
| Proof 전송 크기 | depth × 32 = 640 bytes |
7. SE 상태 조회 인터페이스¶
7.1. GetDeviceInfo¶
Request: INS=0x40, P1=0x00
Response:
DeviceInfo {
se_version: string // SE 펌웨어 버전 "1.2.0"
protocol_version: uint8 // 프로토콜 버전
supported_chains: [uint8] // [0x00(BTC), 0x01(EVM)]
max_psbt_size: uint32 // 최대 PSBT 크기 (bytes)
max_recipients: uint8 // 최대 수신자 수 (WYSIWYS 페이지)
selector_db_count: uint16 // 등록 함수 셀렉터 수
token_db_count: uint16 // 등록 토큰 수
}
7.2. GetPolicyStatus¶
Request: INS=0x40, P1=0x01
Response:
PolicyStatus {
policy_version: uint32 // 현재 정책 버전
policies: [{
id: uint8 // 0x01-0x04
enabled: bool
current_value: bytes(32)
}]
daily_spent: uint64 // 일일 누적 금액
daily_reset_counter: uint32
cooldown_active: bool
cooldown_remaining: uint32 // 초
whitelist_version: uint32
whitelist_root: bytes(32) // Merkle Root
}
7.3. GetSignCounter¶
Request: INS=0x40, P1=0x02
Response:
SignCounterInfo {
global_counter: uint32 // 전체 서명 횟수
window_counter: uint16 // 현재 윈도우 내 서명 수
window_start: uint32 // 윈도우 시작 카운터
last_sign_timestamp_ref: uint32 // 마지막 서명 참조 타임스탬프
}
8. 에러 핸들링 및 복구¶
8.1. 통신 중단 시 세션 복구¶
| 상황 | 처리 |
|---|---|
| QR 스캔 중단 | 오프라인 앱에서 재스캔. SE는 상태 없음 (각 요청 독립 처리) |
| USB-C 연결 끊김 (패킷 전송 중) | USB HID 패킷 첫 패킷부터 재전송. SE는 불완전 데이터 폐기 |
| 서명 후 응답 전달 실패 | SE 서명 카운터는 이미 증가. 오프라인 앱이 GET_SIGNATURE로 재요청 |
| WYSIWYS 표시 중 전원 차단 | SE 상태 초기화. 서명 미수행. 처음부터 재시작 |
8.2. 부분 전송 데이터 처리¶
USB-C 패킷 전송 중 데이터 불완전 수신 시: - SE는 END 커맨드 수신 전까지 데이터 버퍼에 누적 - 30초 내 마지막 청크 미수신 → 버퍼 폐기 - 새 SIGN_REQ 커맨드 수신 시 이전 버퍼 자동 폐기
8.3. SE 잠금 상태 해제¶
| 잠금 유형 | 원인 | 해제 방식 |
|---|---|---|
| PIN 잠금 | PIN 연속 오류 (5회) | PUK 코드 입력 (별도 제공) |
| WYSIWYS 잠금 | 해시 불일치 3회 연속 | 24시간 자동 해제 |
| 정책 쿨다운 | 정책 변경 대기 | 쿨다운 시간 경과 후 자동 |
| 디바이스 잠금 | Admin 원격 잠금 요청 | Admin 쿼럼 서명 + 에어갭 해제 명령 |
본 문서는 Phase 5 System Architecture Design의 일부로, Phase 4 WYSIWYS(wysiwys-design.md) 및 HW 정책 엔진(hardware-policy-engine.md)의 SE 구현 인터페이스를 명세한다. airgap-communication-protocol.md의 USB-C 통신 프로토콜과 연계하여 SE 외부 인터페이스를 정의한다. Phase 6 펌웨어 요구사항의 입력 문서로 활용된다.
관련 문서¶
- 에어갭 통신 프로토콜 설계서 -- 시스템 아키텍처
- 체인 추상화 레이어 인터페이스 정의서 -- 시스템 아키텍처
- 컴포넌트 간 데이터 흐름 및 인터페이스 명세 -- 시스템 아키텍처
- 온라인 대시보드 백엔드 아키텍처 설계서 -- 시스템 아키텍처
- 오프라인 서명 앱 아키텍처 설계서 -- 시스템 아키텍처
- STRIDE 기반 보안 위협 모델 -- 시스템 아키텍처
- 3-Zone 보안 아키텍처 설계서 -- 시스템 아키텍처