Skip to main content

encryption/asymmetric/
x25519.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! X25519 密钥协商(RFC 7748)。
4//!
5//! 私钥(裸标量)/ 对方公钥(Montgomery u 坐标)/ 共享秘密均为 32 字节
6//! (64 位十六进制);私钥在运算时按 RFC 7748 做 clamp。
7//!
8//! 全零共享秘密(对方给出小阶点,如全零 u 坐标)会被拒绝为
9//! [`Error::InvalidKey`]——见 `SharedSecret::was_contributory`。
10
11use rand_core::OsRng;
12use x25519_dalek::{PublicKey, StaticSecret};
13
14use crate::asymmetric::KeyPairHex;
15use crate::contract::{Identified, KeyAgreement};
16use crate::error::{Error, Result};
17use crate::internal::{hex_decode, hex_encode};
18
19const LABEL: &str = "X25519";
20
21/// X25519 密钥协商器(实现 [`crate::contract::KeyAgreement`])。
22#[derive(Debug, Default, Clone, Copy)]
23pub struct X25519Agreement;
24
25impl X25519Agreement {
26    pub const IDENTIFIER: &'static str = "x25519";
27
28    pub fn new() -> Self {
29        Self
30    }
31
32    /// 导出共享秘密(32 字节 hex);对方公钥是小阶点(共享秘密全零)时拒绝。
33    fn agree_hex(private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
34        let private_bytes =
35            parse_32_bytes(private_key_hex).map_err(|_| Error::InvalidKey { label: LABEL })?;
36        let peer_bytes =
37            parse_32_bytes(peer_public_key_hex).map_err(|_| Error::InvalidKey { label: LABEL })?;
38
39        let shared_secret =
40            StaticSecret::from(private_bytes).diffie_hellman(&PublicKey::from(peer_bytes));
41        if !shared_secret.was_contributory() {
42            return Err(Error::InvalidKey { label: LABEL });
43        }
44        Ok(hex_encode(shared_secret.as_bytes()))
45    }
46
47    /// 生成密钥对(私钥 / 公钥均为 32 字节 hex);随机源为系统 CSPRNG。
48    pub fn generate_key_pair_hex() -> Result<KeyPairHex> {
49        let secret = StaticSecret::random_from_rng(OsRng);
50        Ok(KeyPairHex {
51            private_key_hex: hex_encode(secret.as_bytes()),
52            public_key_hex: hex_encode(PublicKey::from(&secret).as_bytes()),
53        })
54    }
55}
56
57/// 十六进制 → 定长 32 字节(标量 / u 坐标都是 32 字节)。
58fn parse_32_bytes(hex: &str) -> Result<[u8; 32]> {
59    hex_decode(hex)?.try_into().map_err(|_| Error::InvalidHex)
60}
61
62impl Identified for X25519Agreement {
63    fn identifier(&self) -> &str {
64        Self::IDENTIFIER
65    }
66}
67
68impl KeyAgreement for X25519Agreement {
69    fn agree(&self, private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
70        Self::agree_hex(private_key_hex, peer_public_key_hex)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// KAT:RFC 7748 §5.2(X25519 定值向量)——alice / bob 私钥与对端公钥
79    /// 导出的共享秘密是同一个定值 K。
80    #[test]
81    fn rfc7748_5_2_shared_secret_matches_vector() {
82        const ALICE_PRIVATE: &str =
83            "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
84        const ALICE_PUBLIC: &str =
85            "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
86        const BOB_PRIVATE: &str =
87            "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
88        const BOB_PUBLIC: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
89        const SHARED: &str = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
90
91        let agreement = X25519Agreement::new();
92        assert_eq!(agreement.agree(ALICE_PRIVATE, BOB_PUBLIC).unwrap(), SHARED);
93        assert_eq!(agreement.agree(BOB_PRIVATE, ALICE_PUBLIC).unwrap(), SHARED);
94        // 两把私钥与对方的公钥交叉相乘,结果相同(对称性)。
95        assert_eq!(
96            agreement.agree(ALICE_PRIVATE, BOB_PUBLIC).unwrap(),
97            agreement.agree(BOB_PRIVATE, ALICE_PUBLIC).unwrap(),
98        );
99    }
100
101    /// KAT:RFC 7748 §6.1(Curve25519 标量乘定值向量,含 clamp 行为)。
102    #[test]
103    fn rfc7748_6_1_scalar_multiplication_matches_vector() {
104        const SCALAR: &str = "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4";
105        const U_COORDINATE: &str =
106            "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c";
107        const OUTPUT: &str = "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552";
108
109        assert_eq!(
110            X25519Agreement::new().agree(SCALAR, U_COORDINATE).unwrap(),
111            OUTPUT
112        );
113    }
114
115    #[test]
116    fn keypair_roundtrip_and_hex_shapes() {
117        let alice = X25519Agreement::generate_key_pair_hex().unwrap();
118        let bob = X25519Agreement::generate_key_pair_hex().unwrap();
119        assert_eq!(alice.private_key_hex.len(), 64);
120        assert_eq!(alice.public_key_hex.len(), 64);
121
122        let agreement = X25519Agreement::new();
123        let left = agreement
124            .agree(&alice.private_key_hex, &bob.public_key_hex)
125            .unwrap();
126        let right = agreement
127            .agree(&bob.private_key_hex, &alice.public_key_hex)
128            .unwrap();
129        assert_eq!(left, right);
130        assert_eq!(left.len(), 64);
131
132        // 换一把私钥 → 共享秘密不同。
133        let mallory = X25519Agreement::generate_key_pair_hex().unwrap();
134        assert_ne!(
135            agreement
136                .agree(&mallory.private_key_hex, &bob.public_key_hex)
137                .unwrap(),
138            left
139        );
140    }
141
142    /// 小阶点(全零 u 坐标)→ 共享秘密全零,必须拒绝。
143    #[test]
144    fn low_order_peer_public_key_is_rejected() {
145        let alice = X25519Agreement::generate_key_pair_hex().unwrap();
146        assert_eq!(
147            X25519Agreement::new()
148                .agree(&alice.private_key_hex, &"00".repeat(32))
149                .unwrap_err(),
150            Error::InvalidKey { label: LABEL }
151        );
152    }
153
154    #[test]
155    fn malformed_keys_are_rejected() {
156        let alice = X25519Agreement::generate_key_pair_hex().unwrap();
157        let agreement = X25519Agreement::new();
158
159        assert_eq!(
160            agreement.agree("zz", &alice.public_key_hex).unwrap_err(),
161            Error::InvalidKey { label: LABEL }
162        );
163        // 31 字节:长度不对。
164        assert_eq!(
165            agreement
166                .agree(&alice.private_key_hex[..62], &alice.public_key_hex)
167                .unwrap_err(),
168            Error::InvalidKey { label: LABEL }
169        );
170    }
171
172    #[test]
173    fn identifier_is_stable() {
174        assert_eq!(X25519Agreement::new().identifier(), "x25519");
175    }
176}