如何调用 ed25519-dalek?
解读
国内后端/区块链/安全方向面试中,“会不会用 ed25519-dalek”≈“能不能在 5 分钟内写出安全、可审计、零拷贝的签名验证代码”。
面试官真正想听的是:
- 你熟悉 Cargo 依赖管理 与 feature 开关(std、serde、batch 等);
- 你理解 Rust 零拷贝切片 & 堆分配 的权衡,不会把私钥到处 clone;
- 你能把 编译期常量长度(32 B 公钥、64 B 签名)与 运行时错误处理 结合起来;
- 你知道 side-channel 防护(常量时间比较、私钥清零)在国密合规场景下的重要性。
回答时先给“最小可用代码”,再主动补充“生产级加固”,节奏感是“秒级编译、毫秒级签名、微秒级验证”。
知识点
- 依赖声明:
ed25519-dalek = { version = "2.1", default-features = false, features = ["std", "zeroize"] },关闭不必要的rand_core/std可减 20 KB 体积。 - 核心类型:
SigningKey、VerifyingKey、Signature;三者均实现Copy/ZeroizeOnDrop,私钥离开作用域即清零。 - 随机数源:
rand_core::OsRng在 SGX/TEE 环境需替换为sgx_rand,否则国密评审会被打回。 - 批量验证:
verify_batch()使用 Straus 线性组合,100 签名验证速度提升 2.3×,但需启用batchfeature 且公钥去重。 - serde 支持:
#[serde(crate = "crate::serde")]可自定义序列化,避免把私钥落到 JSON。 - no_std 落地:嵌入式场景需关
std,用rand_core::block::BlockRng喂种子,栈上私钥不超过 1 KB。 - 错误处理:
SignatureError区分PointDecompressionError与InternalError,方便日志审计。 - 常量时间:
signature.as_bytes() == expected会被优化成短路,必须用bool::from(signature.to_bytes().ct_eq(&expected))。
答案
use ed25519_dalek::{SigningKey, VerifyingKey, Signature, Signer, Verifier};
use rand_core::OsRng;
/// 生产级封装:私钥清零、零拷贝、编译期长度检查
pub struct Ed25519Signer {
inner: SigningKey,
}
impl Ed25519Signer {
/// 生成新私钥
pub fn generate() -> Self {
Self {
inner: SigningKey::generate(&mut OsRng),
}
}
/// 从 32 字节种子恢复,用于 HD 钱包派生
pub fn from_seed(seed: [u8; 32]) -> Self {
Self {
inner: SigningKey::from_bytes(&seed),
}
}
pub fn sign(&self, msg: &[u8]) -> Signature {
self.inner.sign(msg) // 零拷贝,返回 64 B 栈上数组
}
pub fn verifying_key(&self) -> VerifyingKey {
self.inner.verifying_key()
}
}
/// 验证侧无状态,方便放在 enclave 里
pub fn verify(pk_bytes: &[u8; 32], msg: &[u8], sig_bytes: &[u8; 64]) -> Result<(), String> {
let vk = VerifyingKey::from_bytes(pk_bytes).map_err(|_| "InvalidPublicKey")?;
let sig = Signature::from_bytes(sig_bytes);
vk.verify_strict(msg, &sig) // 严格 cofactorless 验证,防 malleability
.map_err(|_| "InvalidSignature")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_and_verify() {
let signer = Ed25519Signer::generate();
let msg = b"hello, 中国链";
let sig = signer.sign(msg);
assert!(verify(&signer.verifying_key().to_bytes(), msg, &sig.to_bytes()).is_ok());
}
}
Cargo.toml 片段
[dependencies]
ed25519-dalek = { version = "2.1", default-features = false, features = ["std", "zeroize"] }
rand_core = { version = "0.6", features = ["getrandom"] }
拓展思考
- 国密合规:ed25519 不在 GM/T 0003 序列,若客户强制 SM2,可用
libsm做桥接,但签名格式需转 ASN.1 DER,否则链上验签失败。 - 侧信道加固:在 ARM Cortex-M 启用
cortex-m-scb::disable_dcache(),防止 cache-timing 攻击 恢复私钥;同时把SigningKey放到#[link_section = ".tdata"]作为线程本地密钥。 - 批量验证的 DoS 边界:
verify_batch失败时攻击者可能混脏数据,需先对公钥做 1 ms 窗口去重,再分片验证,避免 O(n²) 退化。 - 跨语言 FFI:给 Java/Android 提供
jni接口时,私钥只能以jbyteArray零时缓冲区存在,返回后立即memset清零;否则会被 Java GC 延迟暴露。 - WASM 体积:
ed25519-dalek默认带std,前端打包 240 KB;关std+wee_alloc+opt-level=z可压到 78 KB,但OsRng需换成getrandom_custom,用crypto.getRandomValues喂种子。