如何签名 Wasm 模块?
解读
在国内实际业务中,Wasm 模块一旦离开可信编译链路,就可能被篡改或植入恶意代码。面试官问“如何签名”,并不是让你背诵 WebAssembly 规范,而是考察你能否在** Rust 工具链 + 国内合规要求(国密、可信时间戳、等保 2.0)** 的约束下,给出一条可落地的端到端方案:
- 编译期把 Wasm 字节码当成“固件”做摘要;
- 用私钥生成符合国内密码管理条例的签名;
- 运行时先验签、再实例化,失败直接拒绝;
- 整个流程对 Cargo 透明,CI/CD 一键完成。
知识点
- Wasm 二进制格式:自定义段(Custom Section)ID 0,可嵌入任意数据而不影响执行。
- Rust 工具链:
wasm-gc/wasm-opt之后、签名之前必须固定字节序,否则摘要漂移。 - 国密合规:SM2/SM3 替代 ECDSA/SHA-256;硬件加密机(HSM)或云密码服务(阿里云 KMS、腾讯云 KMS)必须支持密钥不出域。
- PKCS#7/CMS:如果浏览器或 Node.js 侧验签,可封装成
.wasm.sig伴生文件,避免改写 Wasm。 - Rust 验签库:
ring目前只支持 ECDSA/RSA;国密需用libsm、sm-crypto或绑定tjos硬件 SDK。 - 运行时集成:
- 浏览器:Web Crypto API 不支持 SM2,需引入
wasm-sm2软实现或服务端预验签。 - 服务端:
wasmtime/wasmer提供Module::deserialize前钩子,验签失败直接返回InstantiationError。
- 浏览器:Web Crypto API 不支持 SM2,需引入
- Cargo 插件:
cargo-wasm-sign(可二次开发)在post-build.rs中注入自定义段,摘要写入build.rs环境变量,保证可重现构建(Reproducible Build)。
答案
给出一套国内可落地的最小闭环(以国密 SM2/SM3 为例,ECDSA 方案同理替换算法即可):
步骤 1:编译并冻结字节码
cargo build --release --target wasm32-unknown-unknown
wasm-opt -Os target/wasm32-unknown-unknown/release/foo.wasm -o foo.wasm
立即计算 SM3 摘要,后续任何再优化都会改变摘要,必须在此步之后完成。
步骤 2:生成签名(HSM 场景)
调用云 KMS “SM2-Sign” API,传入 SM3 摘要,返回 64-byte R‖S。
签名结果写入自定义段 signature,段名为 rust.sig(工具链约定)。
// build.rs 片段
let sig = kms_sm2_sign(&digest)?;
let mut module = fs::read("foo.wasm")?;
module.extend(&custom_section("rust.sig", &sig));
fs::write("foo_signed.wasm", module)?;
步骤 3:运行时验签
use libsm::sm2::signature::Signature;
pub fn verify_wasm(path: &str) -> Result<Vec<u8>, String> {
let data = fs::read(path)?;
let (code, sig) = extract_custom_section(&data, "rust.sig")?;
let pk = get_sm2_public_key(); // 从 KMS 下载或内嵌
let digest = sm3_hash(&code);
Signature::verify(&pk, &digest, &sig)?;
Ok(code)
}
在 wasmtime 实例化前调用:let code = verify_wasm("foo_signed.wasm")?; let module = Module::new(&engine, code)?;
验签失败直接返回 Err,模块永不加载,满足等保“可信执行”要求。
步骤 4:CI/CD 集成
.github/workflows/release.yml 中增加一步:
- name: Sign Wasm
run: |
cargo install cargo-wasm-sign --git https://gitee.com/your-org/cargo-wasm-sign
cargo wasm-sign --hsm-key-id=sm2-key-001 --output=dist/
构建产物只有 foo_signed.wasm 一个文件,部署侧零配置。
拓展思考
- 可重现构建:国内监管开始要求“源码→字节码”可追溯。可在自定义段再写入
git-commit-id + cargo.lock-hash,验签时一并校验,防止“相同源码不同编译器版本”导致摘要不一致。 - 链上存证:把 SM3 摘要写入长安链、FISCO BCOS,实现“发布即审计”,司法存证可直接拉取区块高度作为时间戳。
- 动态链接 Wasm:
wasmtime的component model允许多模块组合。每个.wasm单独签名后,主模块在运行时递归验签依赖,形成“可信依赖树”,解决国内 SaaS 平台‘谁都可以上传插件’的痛点。 - 性能优化:SM2 验签在纯 Rust 下约 1.2 ms/次(2.5 GHz)。若业务对冷启动敏感,可把摘要和签名缓存到共享内存,并配合
mmap零拷贝,做到单次实例化 <50 μs。