如何支持硬件钱包?

解读

硬件钱包(如 Ledger、Trezor、OneKey 等)的核心诉求是:私钥永不离开安全芯片,而 Rust 项目需要完成“与设备安全会话、构造交易、离线签名、回传签名”这一闭环。面试官问“如何支持”,并不是让你背诵 USB 协议,而是考察:

  1. 你是否理解硬件钱包的安全模型(通道加密、验证、防重放);
  2. 能否在不引入 unsafe 或第三方 C 依赖的前提下,用 Rust 完成 HID/CCID/WebUSB 通信、APDU 指令封装、交易元数据序列化;
  3. 是否熟悉国内合规要求(国密 SM2/SM3/SM4、金融终端备案、密码产品型号证书);
  4. 能否把异步 I/O、no_std、嵌入式等 Rust 特色真正落地到硬件场景。

一句话:让 Rust 代码既“安全”又“能通过国内监管”地与黑盒子芯片对话

知识点

  • Rust 加密生态ecdsaed25519-dalekk256p256sm2ringrust-crypto
  • 通信层hidapinusbrusbccidctaphidwebusb
  • APDU 构造与 TLV 解析apdu-rsder-parsersimple_asn1
  • 安全通道:SCP03、ECDH、AES-128-CMAC、PinToken、国密 SM4-GCM
  • 异步与 no_stdembassy-usbusb-devicetokio-usballoc
  • 国内合规:GM/T 0009、GM/T 0016、商用密码产品认证、金融 JR/T 0025
  • 固件升级与防回滚:版本号签名、双区备份、断电保护
  • 错误处理thiserror 定义业务错误、constant_time_eq 防时序攻击

答案

分四层回答,面试官想听“落地路径”,不是堆砌名词。

  1. 链路层:选通道
    国内销售硬件钱包必须过 USB 认证,首选 HID,其次 CCID。Rust 侧用 hidapi(已封装 libusb 的 safe wrapper),禁用 unsafe feature,通过 cargo deny 审计 C 依赖。若走 WebUSB,需把设备描述符里的 bDeviceClass 改为 0xFF,规避浏览器黑名单

  2. 会话层:建安全通道
    按《GM/T 0009》走 SCP03 简化模式:
    a. 主机生成临时密钥对,用设备证书里的 SM2 公钥做 ECDH;
    b. 派生 encKey/macKey
    c. 所有 APDU 使用 SM4-GCM 加密,CMAC 校验。
    Rust 代码用 sm2 crate 完成 ECDH,sm4 crate 做 GCM,禁止自己实现分组模式。会话建立后把 SessionKey 存进 Zeroizing<Vec<u8>>,用完立即 drop

  3. 应用层:构造交易
    把交易元数据(UTXO、nonce、to、value、data、chain_id)序列成 Protobuf 或 RLP,再拆成 224 字节以内的 APDU 块,最后一包带 Le=0x00 触发签名。硬件返回 65 字节 RSV 签名后,用 k256::ecdsa::Signature::from_scalars 还原,再执行 normalize_s() 防延展性攻击。

  4. 合规与审计

    • 代码仓库开启 cargo auditcargo geiger零 unsafe 块才能进主干;
    • 国密算法调用必须通过 国家密码管理局核准的静态库,用 bindgen 生成 -sys crate,再用 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 泄露。

拓展思考

  1. Rust 如何支持“多应用”硬件钱包?
    国内新规要求同一设备可加载不同金融应用(数字人民币、银联、以太坊)。Rust 侧需实现 GlobalPlatform 安全域 加载协议,用 serde 描述 CAP 文件,在 no_std 环境下做静态链接,避免动态加载带来的不可审计性。

  2. 异步签名流水线
    高并发场景(交易所归集)下,单 HID 通道成为瓶颈。可以把多个交易预哈希后一次性发下去,硬件内部用 FIFO 队列签名,Rust 侧用 tokio-usb 异步读取,通过 channel 把签名结果流式抛给上层的 rust-web3ethers-rs,实现“流水线签名 + 零等待广播”。

  3. 防“中间人”钓鱼
    国内曾出现“假 App 替换收款地址”案例。Rust 代码需在 APDU 里带 交易摘要盲化值,硬件屏幕显示 收款地址前后 6 位 + 金额,用户确认后才签名。实现时用 const-str 宏把提示文本写进固件,防止编译器优化掉对比逻辑

  4. 国密双证书体系
    再过两年可能强制双证书(签名证书 + 加密证书)。Rust 侧需同时维护两条证书链,用 const-generic 把证书长度在编译期确定,避免堆分配,满足嵌入式场景 64 KB RAM 限制

把以上四点准备成 3 分钟电梯陈述,面试现场就能从“会用 hidapi”直接跃迁到“能过国密检”,基本锁定 offer。