encryption/asymmetric/
ecdh.rs1use rand_core::OsRng;
14
15use 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
23macro_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 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 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 EcdhP256,
82 "ECDH P-256",
83 "ecdh-p256",
84 p256
85);
86
87ecdh_agreement!(
88 EcdhP384,
90 "ECDH P-384",
91 "ecdh-p384",
92 p384
93);
94
95ecdh_agreement!(
96 EcdhSecp256k1,
98 "ECDH secp256k1",
99 "ecdh-secp256k1",
100 k256
101);
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 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 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 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 assert!(matches!(
147 agreement
148 .agree(&"00".repeat(scalar_len), &bob.public_key_hex)
149 .unwrap_err(),
150 Error::InvalidKey { .. }
151 ));
152 }
153
154 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 #[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 #[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}