如何调用 ed25519-dalek?

解读

国内后端/区块链/安全方向面试中,“会不会用 ed25519-dalek”≈“能不能在 5 分钟内写出安全、可审计、零拷贝的签名验证代码”
面试官真正想听的是:

  1. 你熟悉 Cargo 依赖管理feature 开关(std、serde、batch 等);
  2. 你理解 Rust 零拷贝切片 & 堆分配 的权衡,不会把私钥到处 clone;
  3. 你能把 编译期常量长度(32 B 公钥、64 B 签名)与 运行时错误处理 结合起来;
  4. 你知道 side-channel 防护(常量时间比较、私钥清零)在国密合规场景下的重要性。
    回答时先给“最小可用代码”,再主动补充“生产级加固”,节奏感是“秒级编译、毫秒级签名、微秒级验证”。

知识点

  • 依赖声明ed25519-dalek = { version = "2.1", default-features = false, features = ["std", "zeroize"] },关闭不必要的 rand_core/std 可减 20 KB 体积。
  • 核心类型SigningKeyVerifyingKeySignature;三者均实现 Copy/ZeroizeOnDrop,私钥离开作用域即清零。
  • 随机数源rand_core::OsRng 在 SGX/TEE 环境需替换为 sgx_rand,否则国密评审会被打回。
  • 批量验证verify_batch() 使用 Straus 线性组合,100 签名验证速度提升 2.3×,但需启用 batch feature 且公钥去重。
  • serde 支持#[serde(crate = "crate::serde")] 可自定义序列化,避免把私钥落到 JSON。
  • no_std 落地:嵌入式场景需关 std,用 rand_core::block::BlockRng 喂种子,栈上私钥不超过 1 KB
  • 错误处理SignatureError 区分 PointDecompressionErrorInternalError,方便日志审计。
  • 常量时间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"] }

拓展思考

  1. 国密合规:ed25519 不在 GM/T 0003 序列,若客户强制 SM2,可用 libsm 做桥接,但签名格式需转 ASN.1 DER,否则链上验签失败。
  2. 侧信道加固:在 ARM Cortex-M 启用 cortex-m-scb::disable_dcache(),防止 cache-timing 攻击 恢复私钥;同时把 SigningKey 放到 #[link_section = ".tdata"] 作为线程本地密钥。
  3. 批量验证的 DoS 边界verify_batch 失败时攻击者可能混脏数据,需先对公钥做 1 ms 窗口去重,再分片验证,避免 O(n²) 退化。
  4. 跨语言 FFI:给 Java/Android 提供 jni 接口时,私钥只能以 jbyteArray 零时缓冲区存在,返回后立即 memset 清零;否则会被 Java GC 延迟暴露。
  5. WASM 体积ed25519-dalek 默认带 std,前端打包 240 KB;关 std+wee_alloc+opt-level=z 可压到 78 KB,但 OsRng 需换成 getrandom_custom,用 crypto.getRandomValues 喂种子。