Skip to main content

encryption/asymmetric/
ecdh.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! 静态 ECDH 密钥协商:P-256 / P-384 / secp256k1。
4//!
5//! 十六进制约定(与 ECDSA 部分一致):
6//! - 私钥:裸标量定长 hex(P-256 / secp256k1 64 位,P-384 96 位);
7//! - 对方公钥:SEC1 非压缩点 `04‖X‖Y` hex(130 / 194 位),仅接受非压缩形式;
8//! - 共享秘密:ECDH 原始 x 坐标定长 hex(64 / 96 位)。
9//!
10//! 私钥为 0 或超出阶、公钥不在曲线上(含无法编码的无穷远点)一律拒绝为
11//! [`Error::InvalidKey`]。定值向量见各测试(RFC 5903 §8.1 / §8.2)。
12
13use rand_core::OsRng;
14
15// sec1 的 `ToEncodedPoint` 通过 p256 再导出引入一次,三条曲线共用同一份 trait。
16use p256::elliptic_curve::sec1::ToEncodedPoint;
17
18use crate::asymmetric::KeyPairHex;
19use crate::contract::{Identified, KeyAgreement};
20use crate::error::{Error, Result};
21use crate::internal::{hex_decode, hex_encode};
22
23/// 为 `$curve`(p256 / p384 / k256)生成一个实现 [`KeyAgreement`] 的协商器。
24macro_rules! ecdh_agreement {
25    ($(#[$doc:meta])* $name:ident, $label:literal, $identifier:literal, $curve:ident) => {
26        $(#[$doc])*
27        #[derive(Debug, Default, Clone, Copy)]
28        pub struct $name;
29
30        impl $name {
31            pub const IDENTIFIER: &'static str = $identifier;
32
33            pub fn new() -> Self {
34                Self
35            }
36
37            /// 以己方私钥与对方公钥导出共享秘密(ECDH 原始 x 坐标 hex)。
38            fn agree_hex(private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
39                let private_bytes = hex_decode(private_key_hex)
40                    .map_err(|_| Error::InvalidKey { label: $label })?;
41                let secret_key = $curve::SecretKey::from_slice(&private_bytes)
42                    .map_err(|_| Error::InvalidKey { label: $label })?;
43                let peer_bytes = hex_decode(peer_public_key_hex)
44                    .map_err(|_| Error::InvalidKey { label: $label })?;
45                let peer_key = $curve::PublicKey::from_sec1_bytes(&peer_bytes)
46                    .map_err(|_| Error::InvalidKey { label: $label })?;
47
48                let shared_secret =
49                    $curve::ecdh::diffie_hellman(secret_key.to_nonzero_scalar(), peer_key.as_affine());
50                Ok(hex_encode(shared_secret.raw_secret_bytes()))
51            }
52
53            /// 生成密钥对:私钥 = 标量 hex,公钥 = SEC1 非压缩点 `04‖X‖Y` hex;
54            /// 随机源为系统 CSPRNG。
55            pub fn generate_key_pair_hex() -> Result<KeyPairHex> {
56                let secret_key = $curve::SecretKey::random(&mut OsRng);
57                let public_key = secret_key.public_key().to_encoded_point(false);
58                Ok(KeyPairHex {
59                    private_key_hex: hex_encode(secret_key.to_bytes().as_slice()),
60                    public_key_hex: hex_encode(public_key.as_bytes()),
61                })
62            }
63        }
64
65        impl Identified for $name {
66            fn identifier(&self) -> &str {
67                Self::IDENTIFIER
68            }
69        }
70
71        impl KeyAgreement for $name {
72            fn agree(&self, private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
73                Self::agree_hex(private_key_hex, peer_public_key_hex)
74            }
75        }
76    };
77}
78
79ecdh_agreement!(
80    /// ECDH P-256。
81    EcdhP256,
82    "ECDH P-256",
83    "ecdh-p256",
84    p256
85);
86
87ecdh_agreement!(
88    /// ECDH P-384。
89    EcdhP384,
90    "ECDH P-384",
91    "ecdh-p384",
92    p384
93);
94
95ecdh_agreement!(
96    /// ECDH secp256k1。
97    EcdhSecp256k1,
98    "ECDH secp256k1",
99    "ecdh-secp256k1",
100    k256
101);
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    /// roundtrip + 形状检查 + 双向一致 + 错私钥不同 + 非法点拒绝。
108    fn assert_agreement(agreement: &dyn KeyAgreement) {
109        let alice = keypair_for(agreement).unwrap();
110        let bob = keypair_for(agreement).unwrap();
111        let scalar_len = alice.private_key_hex.len();
112        assert_eq!(alice.public_key_hex.len(), scalar_len * 2 + 2);
113        assert!(alice.public_key_hex.starts_with("04"));
114
115        let left = agreement
116            .agree(&alice.private_key_hex, &bob.public_key_hex)
117            .unwrap();
118        let right = agreement
119            .agree(&bob.private_key_hex, &alice.public_key_hex)
120            .unwrap();
121        assert_eq!(left, right);
122        // 共享秘密是原始 x 坐标:与标量同长(32 / 48 字节)。
123        assert_eq!(left.len(), scalar_len);
124
125        let mallory = keypair_for(agreement).unwrap();
126        assert_ne!(
127            agreement
128                .agree(&mallory.private_key_hex, &bob.public_key_hex)
129                .unwrap(),
130            left
131        );
132
133        // 非曲线上的点(全零坐标)与坏长度公钥都拒绝。
134        let zeros = format!("04{}", "00".repeat(scalar_len));
135        assert!(matches!(
136            agreement.agree(&alice.private_key_hex, &zeros).unwrap_err(),
137            Error::InvalidKey { .. }
138        ));
139        assert!(matches!(
140            agreement
141                .agree(&alice.private_key_hex, "04deadbeef")
142                .unwrap_err(),
143            Error::InvalidKey { .. }
144        ));
145        // 私钥为 0 也拒绝(不属于 [1, n-1])。
146        assert!(matches!(
147            agreement
148                .agree(&"00".repeat(scalar_len), &bob.public_key_hex)
149                .unwrap_err(),
150            Error::InvalidKey { .. }
151        ));
152    }
153
154    /// 同一实现另生一对密钥(协商器本身不绑定密钥,按标识分派)。
155    fn keypair_for(agreement: &dyn KeyAgreement) -> Result<KeyPairHex> {
156        match agreement.identifier() {
157            "ecdh-p256" => EcdhP256::generate_key_pair_hex(),
158            "ecdh-p384" => EcdhP384::generate_key_pair_hex(),
159            "ecdh-secp256k1" => EcdhSecp256k1::generate_key_pair_hex(),
160            other => panic!("unexpected identifier {other}"),
161        }
162    }
163
164    #[test]
165    fn p256_roundtrip_and_hex_shapes() {
166        assert_agreement(&EcdhP256::new());
167    }
168
169    #[test]
170    fn p384_roundtrip_and_hex_shapes() {
171        assert_agreement(&EcdhP384::new());
172    }
173
174    #[test]
175    fn secp256k1_roundtrip_and_hex_shapes() {
176        assert_agreement(&EcdhSecp256k1::new());
177    }
178
179    /// KAT:RFC 5903 §8.1(256 位随机 ECP 组 / NIST P-256)。
180    /// i / r 为双方私钥,g^r / g^i 为对端公钥,共享秘密 = girx。
181    #[test]
182    fn rfc5903_8_1_p256_shared_secret_matches_vector() {
183        const INITIATOR_PRIVATE: &str =
184            "c88f01f510d9ac3f70a292daa2316de544e9aab8afe84049c62a9c57862d1433";
185        const INITIATOR_PUBLIC: &str = concat!(
186            "04dad0b65394221cf9b051e1feca5787d098dfe637fc90b9ef945d0c3772581180",
187            "5271a0461cdb8252d61f1c456fa3e59ab1f45b33accf5f58389e0577b8990bb3",
188        );
189        const RESPONDER_PRIVATE: &str =
190            "c6ef9c5d78ae012a011164acb397ce2088685d8f06bf9be0b283ab46476bee53";
191        const RESPONDER_PUBLIC: &str = concat!(
192            "04d12dfb5289c8d4f81208b70270398c342296970a0bccb74c736fc7554494bf63",
193            "56fbf3ca366cc23e8157854c13c58d6aac23f046ada30f8353e74f33039872ab",
194        );
195        const SHARED: &str = "d6840f6b42f6edafd13116e0e12565202fef8e9ece7dce03812464d04b9442de";
196
197        let agreement = EcdhP256::new();
198        assert_eq!(
199            agreement
200                .agree(INITIATOR_PRIVATE, RESPONDER_PUBLIC)
201                .unwrap(),
202            SHARED
203        );
204        assert_eq!(
205            agreement
206                .agree(RESPONDER_PRIVATE, INITIATOR_PUBLIC)
207                .unwrap(),
208            SHARED
209        );
210    }
211
212    /// KAT:RFC 5903 §8.2(384 位随机 ECP 组 / NIST P-384)。
213    #[test]
214    fn rfc5903_8_2_p384_shared_secret_matches_vector() {
215        const INITIATOR_PRIVATE: &str = concat!(
216            "099f3c7034d4a2c699884d73a375a67f7624ef7c6b3c0f160647b67414dce655",
217            "e35b538041e649ee3faef896783ab194",
218        );
219        const INITIATOR_PUBLIC: &str = concat!(
220            "04667842d7d180ac2cde6f74f37551f55755c7645c20ef73e31634fe72b4c55ee6",
221            "de3ac808acb4bdb4c88732aee95f41aa",
222            "9482ed1fc0eeb9cafc4984625ccfc23f65032149e0e144ada024181535a0f38e",
223            "eb9fcff3c2c947dae69b4c634573a81c",
224        );
225        const RESPONDER_PRIVATE: &str = concat!(
226            "41cb0779b4bdb85d47846725fbec3c9430fab46cc8dc5060855cc9bda0aa2942",
227            "e0308312916b8ed2960e4bd55a7448fc",
228        );
229        const RESPONDER_PUBLIC: &str = concat!(
230            "04e558dbef53eecde3d3fccfc1aea08a89a987475d12fd950d83cfa41732bc509d",
231            "0d1ac43a0336def96fda41d0774a3571",
232            "dcfbec7aacf3196472169e838430367f66eebe3c6e70c416dd5f0c68759dd1ff",
233            "f83fa40142209dff5eaad96db9e6386c",
234        );
235        const SHARED: &str = concat!(
236            "11187331c279962d93d604243fd592cb9d0a926f422e47187521287e7156c5c4",
237            "d603135569b9e9d09cf5d4a270f59746",
238        );
239
240        let agreement = EcdhP384::new();
241        assert_eq!(
242            agreement
243                .agree(INITIATOR_PRIVATE, RESPONDER_PUBLIC)
244                .unwrap(),
245            SHARED
246        );
247        assert_eq!(
248            agreement
249                .agree(RESPONDER_PRIVATE, INITIATOR_PUBLIC)
250                .unwrap(),
251            SHARED
252        );
253    }
254
255    #[test]
256    fn identifiers_are_stable() {
257        assert_eq!(EcdhP256::new().identifier(), "ecdh-p256");
258        assert_eq!(EcdhP384::new().identifier(), "ecdh-p384");
259        assert_eq!(EcdhSecp256k1::new().identifier(), "ecdh-secp256k1");
260    }
261}