You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在curve25519-dalek中X25519转Ed25519签名验证失败问题排查

X25519转Ed25519密钥不匹配问题排查

问题背景

仅持有X25519密钥对,需基于它创建并验证Ed25519签名,因此尝试将X25519公钥转为对应Ed25519验证密钥,同时转换X25519私钥为Ed25519签名密钥。参考公开实现基于curve25519-dalek库编写转换函数后,测试发现转换后的Ed25519验证密钥与私钥派生的验证密钥不匹配,签名验证失败。

原公钥转换函数

pub(crate) fn x25519_pub_to_ed25519_ver(
    public_key: [u8; 32],
) -> Result<VerifyingKey, SignatureError> {
    match MontgomeryPoint(public_key).to_edwards(SIGN_BIT) {
        None => Err(SignatureError::new()),
        Some(key) => VerifyingKey::from_bytes(key.compress().as_bytes()),
    }
}

原私钥转换函数

pub(crate) fn x25519_priv_to_ed25519_sig(private_key: &StaticSecret) -> SigningKey {
    let clamped = clamp_integer(private_key.to_bytes());
    let signing_key = SigningKey::from_bytes(&clamped);
    let test_public_key = signing_key.verifying_key().to_bytes();

    if (test_public_key[31] & 0x80) >> 7 == SIGN_BIT {
        signing_key
    } else {
        let scalar = Scalar::from_bytes_mod_order(clamped);
        let neg = scalar.invert();
        SigningKey::from_bytes(&neg.to_bytes())
    }
}

测试代码

const TEST_MESSAGE: &[u8] = b"Das Pferd frisst keinen Gurkensalat.";

fn main() {
    let private_key = StaticSecret::random();
    println!("X25519 private key: {:02x?}", private_key.to_bytes());
    let public_key = PublicKey::from(&private_key);

    let signing_key = x25519_priv_to_ed25519_sig(&private_key);
    println!("Ed25519 signing key: {:02x?}", signing_key.to_bytes());
    let signature = signing_key.sign(TEST_MESSAGE);

    let verifying_key = x25519_pub_to_ed25519_ver(public_key.to_bytes()).unwrap();
    println!(
        "Transformed verifying key: {:02x?}",
        verifying_key.to_bytes()
    );
    println!(
        "Real verifying key: {:02x?}",
        signing_key.verifying_key().to_bytes()
    );
    let result = verifying_key.verify(TEST_MESSAGE, &signature);
    assert!(result.is_ok());
}

错误原因

私钥转换函数中取反逻辑完全错误:原代码使用scalar.invert()求标量的乘法逆元,但实际需要的是求标量的负元(即-scalar mod l,l为Curve25519的曲线阶)。

X25519私钥转Ed25519时,若派生公钥的符号位与转换公钥时指定的SIGN_BIT不匹配,需将私钥标量取负,这样对应的Edwards公钥也会取负,从而符号位匹配。逆元操作和取负的数学意义完全不同,直接导致私钥转换后对应的公钥与X25519公钥转换来的Edwards公钥不匹配。

额外注意:若自定义的clamp_integer未遵循X25519标准clamp规则(第0字节最低3位置0,第31字节最高位置0、次高位置1),也会引发密钥不匹配问题。

修正后的私钥转换函数

pub(crate) fn x25519_priv_to_ed25519_sig(private_key: &StaticSecret) -> SigningKey {
    let mut clamped = private_key.to_bytes();
    // 执行X25519标准clamp操作
    clamped[0] &= 0xF8;
    clamped[31] &= 0x7F;
    clamped[31] |= 0x40;

    let signing_key = SigningKey::from_bytes(&clamped);
    let test_pub = signing_key.verifying_key().to_bytes();

    if (test_pub[31] & 0x80) >> 7 == SIGN_BIT {
        signing_key
    } else {
        // 计算标量负元而非逆元
        let scalar = Scalar::from_bytes_mod_order(clamped);
        let neg_scalar = -scalar;
        SigningKey::from_bytes(&neg_scalar.to_bytes())
    }
}

验证逻辑说明

  1. X25519 Montgomery公钥转Edwards公钥时,to_edwards(SIGN_BIT)强制设置Edwards点的符号位为指定值,这一步逻辑正确。
  2. 私钥转换时先做标准clamp,生成Ed25519签名密钥后检查公钥符号位,不匹配则取负私钥标量,确保转换后的公钥与X25519公钥转换得到的Edwards公钥完全一致,从而签名验证可通过。

内容的提问来源于stack exchange,提问作者user3035850

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 21:44:55