하드웨어 지갑 펌웨어: 도메인이 아니라 서명을 검증하라
실측: Trezor 2.9.1(1,614,848바이트)을 받아 공식 지문을 재계산했습니다. 파일 SHA-256과 공개 값은 다르지만 둘 다 맞습니다. 2-of-3 서명 검증까지.
2026년 10월 29일 data.trezor.io에서 Trezor T2T1 펌웨어 2.9.1을 받았습니다. 1,614,848바이트, SHA-256은 6a84c200f13b564ffc2f16c2a6806272b0e7561d04d434f55b495d15c5a0c027입니다. 그런데 벤더의 releases.json에 있는 같은 버전의 지문은 aaa616c14904cf6eb940497d7fd5da8735ab0d0ecdebd3f6ced49fff0ce278a0입니다. 한 파일에 두 개의 다른 값이 있고 둘 다 맞습니다. 펌웨어에 sha256sum을 돌려 공개 값과 비교했는데 달라서 변조된 파일을 받았다고 생각한 적이 있다면, 이유가 이것입니다.
두 개의 해시, 두 가지 역할
SHA-256은 디스크의 바이트를 덮는 전송 검사입니다. 다운로드가 잘리거나 전송 중 깨지지 않았음을 알려 줍니다. 지문은 전혀 다른 것으로, 기기의 부트로더가 펌웨어 헤더에 대해 계산하는 값이며 벤더 서명이 서명하는 값입니다. 구체적으로 2.9.1 파일은 세 부분입니다. 4,608바이트 벤더 헤더(매직 TRZV), 1,024바이트 펌웨어 헤더(TRZF, 오프셋 4,608에서 시작), 그리고 1,609,216바이트 코드입니다. 헤더에는 버전 2.9.1.0, fix_version 2.9.0.0, hw_model T2T1, 단조 카운터 2, 만료 필드 0, 32바이트 코드 해시 슬롯 16개(13개 채워짐: 128 KiB 청크 12개와 마지막 36,352바이트), 이 세대에서 비어 있는 ECDSA 서명 슬롯 3개, 1바이트 sigmask, 64바이트 서명이 들어 있습니다.
다섯 단계로 직접 재현
1) 바이트열 TRZF를 찾습니다. 여기서는 오프셋 4,608입니다. 2) 이어지는 12바이트에서 header_len(1,024)과 code_length(1,609,216)을 읽습니다. 3) 코드를 128 KiB 청크로 자르되 경계를 코드 시작이 아니라 파일 시작에 맞춥니다. 그래서 청크 0은 131,072 − 5,632 = 125,440바이트이고, 코드 시작부터 그냥 자르면 13개 해시가 모두 어긋납니다. 4) 헤더를 재구성합니다. 13개 해시를 오프셋 32의 512바이트 영역에 넣고, 544~739(비어 있는 ECDSA 영역)와 마지막 65바이트(sigmask와 서명)를 0으로 만듭니다. 5) 1,024바이트 헤더를 blake2s로 해시합니다. 결과는 aaa616c14904cf6eb940497d7fd5da8735ab0d0ecdebd3f6ced49fff0ce278a0로 releases.json의 문자열과 정확히 같습니다. 같은 절차를 2.8.9, 2.8.10, 2.9.0에 적용하면 각 릴리스의 공개 값에 닿습니다. 방법이 운이 아니라 맞다는 것을 그렇게 확인했습니다. (모네로 확인 대기)
서명이 자물쇠이고, 그것도 2-of-3
헤더의 sigmask 바이트는 3으로, 0x20, 0x40, 0x60에 있는 벤더 공개키 3개 중 0번과 1번을 선택했다는 뜻입니다. Trezor는 서명자 공개키를 Edwards 점으로 더하는 임계값 방식인 CoSi를 쓰기 때문에, 64바이트 서명은 합성 키 52ef8f8496c888a45aa526cd980105f1dd6ce7b435f0be8fde86152c244a7805 아래에서 평범한 Ed25519 서명으로 검증됩니다. 곡선 연산을 구현하고 먼저 RFC 8032 테스트 벡터로 확인한 뒤 다이제스트에 적용한 결과는 true였습니다. 키 0만 또는 키 1만으로 같은 검증을 하면 false입니다. 2-of-3이지 '셋 중 아무 하나'가 아닙니다. 4,608바이트 벤더 헤더도 같은 방식으로 서명되어 도구에 내장된 부트로더 키 3개 중 2개로 검증되며 다이제스트는 abb773af7aa4c475…입니다. 사슬은 벤더 헤더 → 펌웨어이고, 닻은 서버가 아니라 기기 위의 키에 있습니다. (모네로 확인 대기)
1바이트만 바꿔도 실패한다
이 검증 자체를 확인하려고 오프셋 100,000에서 1비트를 뒤집고, 다운로드 소스를 장악한 공격자처럼 13개 청크 해시를 모두 다시 계산해 헤더를 재구성한 뒤 검증기를 다시 돌렸습니다. 다이제스트는 aaa616…에서 249da0562e5ce9c15f3537cec046894c46c488cd6017850c79ed66053a4e2218로 바뀌었고 서명 검증은 false를 반환했습니다. 설계의 요점이 이것입니다. 해시 재계산은 공짜지만 서명 위조는 불가능합니다. 다운그레이드는 다른 장치가 막습니다. 헤더의 단조 카운터가 2이고 더 낮은 값의 이미지는 거부됩니다. '알려진 취약점이 있는 버전으로 되돌리기' 공격을 막는 것이 이것입니다. 필드 맵이 실제임을 보여 주는 디테일도 있습니다. 만료 필드는 0이고 벤더 자체 파서에는 'expiry is not used now'라는 주석이 붙어 있습니다. (모네로 확인 대기)
'공식 채널'이 실제로 사 주는 것
불편한 부분이 여기입니다. 벤더의 키는 다운로드에 전혀 들어 있지 않습니다. 96바이트 벤더 공개키는 2.8.9, 2.8.10, 2.9.0, 2.9.1에서 바이트 단위로 동일합니다(그 SHA-256은 b56e59ef7eaf8217…). 벤더 헤더가 릴리스마다 내려오는 것이 아니라 제조 시 기기에 고정되기 때문입니다. 그래서 악성 미러가 줄 수 있는 것은 진짜 서명된 이미지, 오래된 서명 이미지, 깨진 패키지 중 하나이고 어느 것도 자금을 훔치지 못합니다. 부트로더가 유효한 서명을 받아들이거나 거절할 뿐입니다. 적대적 페이지가 실제로 훔칠 수 있는 것은 시드입니다. '펌웨어 업데이터'를 자처하며 24단어를 요구하는 페이지가 현실적 공격이고, 거기에는 암호학적 접점이 전혀 없습니다. 다운로드 페이지가 중요한 이유는 피싱 위생이지 신뢰가 아닙니다. 실제 위험은 셋입니다. 시드를 노리는 업데이트 마법사, 지갑과 dApp 뒤의 데스크톱/npm 공급망(2023년 12월 Ledger Connect Kit, 2024년 12월 @solana/web3.js), 그리고 남의 벤더 헤더를 가진 위조·리퍼비시 기기입니다. 덧붙이면 2.9.1에는 bitcoin-only 빌드가 있고 지문은 77b53ba1e381fcf8…입니다. 지문 대조는 잘못된 변형을 플래싱하는 사고도 막아 줍니다.
Ledger는 신뢰의 닻을 다른 곳에 둔다
Ledger의 모델은 이 방식의 열등한 버전이 아니라 다른 맞바꿈입니다. 키는 보안 요소에 있고 펌웨어는 보안 요소가 검증하며, 동반 앱은 인증 기반 정품 확인을 수행하고, 앱 계층은 오픈소스지만 요소 내부 OS는 아닙니다. 포기하는 것은 제가 위에서 한 일 그대로입니다. 제3자가 재현할 다이제스트도, 공개 키로 확인할 임계값 서명도 없어 검증은 기기와 벤더에 위임됩니다. 얻는 것은 범용 마이크로컨트롤러에 키 소재가 놓이지 않는다는 점입니다. 어느 모델도 지갑을 실제로 비우는 공격, 즉 사용자가 시드를 기기가 아닌 무언가에 입력하는 일은 막지 못합니다. (모네로 확인 대기)
스왑은 여기에 닿지 않는다 — 서명하기 전까지는
스왑 주문이 받는 필드는 다섯 개입니다. fromAsset, toAsset, fromAmount, toAddress, refundAddress. 지갑 연결도 서명도 시드도 없습니다. 그래서 펌웨어 위험은 교환하는 순간이 아니라 지갑에서 밖으로 보내는 순간에 있습니다. 규모를 보면 오늘 아침 카탈로그는 516개 자산, 74개 네트워크, 415개 티커, 61개 스테이블코인, 추가 ID가 필요한 자산 6개였습니다. 10:07 UTC 견적: 100 USDT-TRC20은 0.14687052 XMR(XMR당 680.87달러로 현물 597.08달러보다 14.03% 높고 거의 전부가 주문당 하한), 1 XMR은 578.800581 USDT(출금 쪽은 현물보다 3.06% 낮음), 0.05 BTC는 6.61007527 XMR로 132.20151 XMR/BTC, 현물은 80,072달러입니다. 모두 작성 시점 값이며 실시간 숫자는 페이지만 제공합니다. (XMR→USDT 출구 루트)
플래싱 전 여섯 가지 점검
1) 벤더 자체 도메인에서만 받고, 단어를 요구하는 '펌웨어 업데이터'는 공격으로 취급합니다. 2) 지문을 계산해 releases.json과 비교합니다. `trezorctl firmware verify trezor-t2t1-2.9.1.bin --fingerprint aaa616c1…` 한 줄로 서명과 지문을 함께 확인합니다. 3) 임계값을 확인합니다. sigmask 3에 키 3개는 공동 서명자가 둘이라는 뜻입니다. 4) 다운그레이드가 아닌지 확인합니다. 단조 카운터는 뒤로 가면 안 됩니다. 5) 공식 앱에서 USB로 플래싱하고 버전 문자열을 노트북 화면이 아니라 기기 화면에서 확인합니다. 6) 재부팅 후 기기에서 버전과 지문을 다시 읽습니다. 플래싱한 것과 다른 값을 보고하면 그 하드웨어를 의심 대상으로 다루십시오. (모네로 확인 대기)
위협 모델은 한 줄로 압축됩니다. 펌웨어 공격에는 벤더의 개인키나 사용자의 시드가 필요하고, 둘 다 브라우저 밖에 있습니다. 그래서 도메인도 자물쇠도 다운로드 페이지도 의지할 통제가 되지 못합니다. 그것들은 도중에 피싱을 당하는지만 결정합니다. 위 산술로 산출물을 한 번 검증하고 시드를 기기 안에 두면, 하드웨어 지갑의 가장 무서운 부분은 미스터리가 아니라 체크박스가 됩니다.