如何支持硬件钱包?
解读
硬件钱包(如 Ledger、Trezor、OneKey 等)的核心诉求是:私钥永不离开安全芯片,而 Rust 项目需要完成“与设备安全会话、构造交易、离线签名、回传签名”这一闭环。面试官问“如何支持”,并不是让你背诵 USB 协议,而是考察:
- 你是否理解硬件钱包的安全模型(通道加密、验证、防重放);
- 能否在不引入 unsafe 或第三方 C 依赖的前提下,用 Rust 完成 HID/CCID/WebUSB 通信、APDU 指令封装、交易元数据序列化;
- 是否熟悉国内合规要求(国密 SM2/SM3/SM4、金融终端备案、密码产品型号证书);
- 能否把异步 I/O、no_std、嵌入式等 Rust 特色真正落地到硬件场景。
一句话:让 Rust 代码既“安全”又“能通过国内监管”地与黑盒子芯片对话。
知识点
- Rust 加密生态:
ecdsa、ed25519-dalek、k256、p256、sm2、ring、rust-crypto - 通信层:
hidapi、nusb、rusb、ccid、ctaphid、webusb - APDU 构造与 TLV 解析:
apdu-rs、der-parser、simple_asn1 - 安全通道:SCP03、ECDH、AES-128-CMAC、PinToken、国密 SM4-GCM
- 异步与 no_std:
embassy-usb、usb-device、tokio-usb、alloc - 国内合规:GM/T 0009、GM/T 0016、商用密码产品认证、金融 JR/T 0025
- 固件升级与防回滚:版本号签名、双区备份、断电保护
- 错误处理:
thiserror定义业务错误、constant_time_eq防时序攻击
答案
分四层回答,面试官想听“落地路径”,不是堆砌名词。
-
链路层:选通道
国内销售硬件钱包必须过 USB 认证,首选 HID,其次 CCID。Rust 侧用hidapi(已封装libusb的 safe wrapper),禁用 unsafe feature,通过cargo deny审计 C 依赖。若走 WebUSB,需把设备描述符里的bDeviceClass改为 0xFF,规避浏览器黑名单。 -
会话层:建安全通道
按《GM/T 0009》走 SCP03 简化模式:
a. 主机生成临时密钥对,用设备证书里的 SM2 公钥做 ECDH;
b. 派生encKey/macKey;
c. 所有 APDU 使用SM4-GCM加密,CMAC校验。
Rust 代码用sm2crate 完成 ECDH,sm4crate 做 GCM,禁止自己实现分组模式。会话建立后把SessionKey存进Zeroizing<Vec<u8>>,用完立即drop。 -
应用层:构造交易
把交易元数据(UTXO、nonce、to、value、data、chain_id)序列成 Protobuf 或 RLP,再拆成 224 字节以内的 APDU 块,最后一包带 Le=0x00 触发签名。硬件返回 65 字节 RSV 签名后,用k256::ecdsa::Signature::from_scalars还原,再执行normalize_s()防延展性攻击。 -
合规与审计
- 代码仓库开启
cargo audit、cargo geiger,零 unsafe 块才能进主干; - 国密算法调用必须通过 国家密码管理局核准的静态库,用
bindgen生成-syscrate,再用unsafe封装成 safe API,单独写一份《国密接口安全评估报告》 供过检; - 固件升级包用 双签名(厂商 SM2 + 用户助记词派生 SM2),升级前验签,失败即进“砖模式”,防止供应链攻击。
- 代码仓库开启
示例骨架(HID 通道,国密):
use hidapi::HidApi;
use sm2::ecc::{EcPoint, EccCtx};
use sm4::cipher::{KeyInit, StreamCipher};
use zeroize::Zeroizing;
const LEDGER_VID: u16 = 0x2c97;
const LEDGER_PID: u16 = 0x5011;
fn open_device() -> Result<HidDevice, Box<dyn std::error::Error>> {
let api = HidApi::new()?;
let dev = api.open(LEDGER_VID, LEDGER_PID)?;
Ok(dev)
}
fn establish_scpp03_session(dev: &HidDevice) -> Result<Zeroizing<[u8; 32]>, Box<dyn std::error::Error>> {
let host_key = sm2::generate_keypair();
let cert = get_device_certificate(dev)?; // 读 0xE0 0xC4
let shared = sm2::ecdh(&host_key, &cert.pubkey)?;
let session_key = kdf_gmt_003_2012(&shared, b"SM4 ENC MAC")?;
Ok(Zeroizing::new(session_key))
}
关键点:所有密钥材料都在栈上,函数返回前自动 zeroize,防止 swap 泄露。
拓展思考
-
Rust 如何支持“多应用”硬件钱包?
国内新规要求同一设备可加载不同金融应用(数字人民币、银联、以太坊)。Rust 侧需实现 GlobalPlatform 安全域 加载协议,用serde描述 CAP 文件,在 no_std 环境下做静态链接,避免动态加载带来的不可审计性。 -
异步签名流水线
高并发场景(交易所归集)下,单 HID 通道成为瓶颈。可以把多个交易预哈希后一次性发下去,硬件内部用 FIFO 队列签名,Rust 侧用tokio-usb异步读取,通过 channel 把签名结果流式抛给上层的rust-web3或ethers-rs,实现“流水线签名 + 零等待广播”。 -
防“中间人”钓鱼
国内曾出现“假 App 替换收款地址”案例。Rust 代码需在 APDU 里带 交易摘要盲化值,硬件屏幕显示 收款地址前后 6 位 + 金额,用户确认后才签名。实现时用const-str宏把提示文本写进固件,防止编译器优化掉对比逻辑。 -
国密双证书体系
再过两年可能强制双证书(签名证书 + 加密证书)。Rust 侧需同时维护两条证书链,用 const-generic 把证书长度在编译期确定,避免堆分配,满足嵌入式场景 64 KB RAM 限制。
把以上四点准备成 3 分钟电梯陈述,面试现场就能从“会用 hidapi”直接跃迁到“能过国密检”,基本锁定 offer。