1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
2use chrono::{DateTime, Utc};
3use p256::elliptic_curve::rand_core::OsRng;
4use p256::elliptic_curve::sec1::ToEncodedPoint;
5use serde::{Deserialize, Serialize};
6use zeroize::Zeroizing;
7
8use auths_crypto::{CurveType, TypedSeed};
9use auths_keri::KeriPublicKey;
10
11use crate::error::ProtocolError;
12use crate::token::PairingToken;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PairingResponse {
22 pub short_code: String,
23 pub device_ephemeral_pubkey: String,
24 pub device_signing_pubkey: String,
25 #[serde(default)]
28 pub curve: CurveTag,
29 pub device_did: String,
30 pub signature: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub device_name: Option<String>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(rename_all = "lowercase")]
42pub enum CurveTag {
43 Ed25519,
44 #[default]
45 P256,
46}
47
48impl From<CurveTag> for CurveType {
49 fn from(tag: CurveTag) -> Self {
50 match tag {
51 CurveTag::Ed25519 => CurveType::Ed25519,
52 CurveTag::P256 => CurveType::P256,
53 }
54 }
55}
56
57impl From<CurveType> for CurveTag {
58 fn from(curve: CurveType) -> Self {
59 match curve {
60 CurveType::Ed25519 => CurveTag::Ed25519,
61 CurveType::P256 => CurveTag::P256,
62 }
63 }
64}
65
66impl PairingResponse {
67 pub fn create(
81 now: DateTime<Utc>,
82 token: &PairingToken,
83 device_seed: &TypedSeed,
84 device_pubkey: &[u8],
85 device_did: String,
86 device_name: Option<String>,
87 ) -> Result<(Self, Zeroizing<[u8; 32]>), ProtocolError> {
88 if token.is_expired(now) {
89 return Err(ProtocolError::Expired);
90 }
91
92 let expected_len = device_seed.curve().public_key_len();
93 if device_pubkey.len() != expected_len {
94 return Err(ProtocolError::KeyExchangeFailed(format!(
95 "device_pubkey length {} does not match {} (expected {} bytes)",
96 device_pubkey.len(),
97 device_seed.curve(),
98 expected_len,
99 )));
100 }
101
102 let device_ecdh_secret = p256::ecdh::EphemeralSecret::random(&mut OsRng);
103 let device_ecdh_public = device_ecdh_secret.public_key();
104
105 let initiator_ecdh_bytes = token.ephemeral_pubkey_bytes()?;
106 let initiator_pk =
107 p256::PublicKey::from_sec1_bytes(&initiator_ecdh_bytes).map_err(|_| {
108 ProtocolError::KeyExchangeFailed(
109 "Invalid initiator P-256 ephemeral pubkey (SEC1 decode failed)".to_string(),
110 )
111 })?;
112
113 let shared = device_ecdh_secret.diffie_hellman(&initiator_pk);
114 let shared_bytes: [u8; 32] =
115 shared
116 .raw_secret_bytes()
117 .as_slice()
118 .try_into()
119 .map_err(|_| {
120 ProtocolError::KeyExchangeFailed("Shared secret not 32 bytes".to_string())
121 })?;
122 let shared_secret = Zeroizing::new(shared_bytes);
123
124 let device_signing_pubkey = URL_SAFE_NO_PAD.encode(device_pubkey);
125 let device_ecdh_pubkey_bytes = device_ecdh_public.to_encoded_point(true);
126 let device_ephemeral_pubkey_str =
127 URL_SAFE_NO_PAD.encode(device_ecdh_pubkey_bytes.as_bytes());
128
129 let mut message = Vec::new();
130 message.extend_from_slice(token.session_id.as_bytes());
131 message.extend_from_slice(token.short_code.as_bytes());
132 message.extend_from_slice(&initiator_ecdh_bytes);
133 message.extend_from_slice(device_ecdh_pubkey_bytes.as_bytes());
134
135 let sig_bytes = typed_sign_sync(device_seed, &message)?;
136 let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
137
138 let response = PairingResponse {
139 short_code: token.short_code.clone(),
140 device_ephemeral_pubkey: device_ephemeral_pubkey_str,
141 device_signing_pubkey,
142 curve: device_seed.curve().into(),
143 device_did,
144 signature,
145 device_name,
146 };
147
148 Ok((response, shared_secret))
149 }
150
151 pub fn verify(&self, now: DateTime<Utc>, token: &PairingToken) -> Result<(), ProtocolError> {
157 if token.is_expired(now) {
158 return Err(ProtocolError::Expired);
159 }
160
161 let initiator_ecdh_bytes = token.ephemeral_pubkey_bytes()?;
162 let device_ecdh_bytes = self.device_ephemeral_pubkey_bytes()?;
163 let device_signing_bytes = self.device_signing_pubkey_bytes()?;
164 let signature_bytes = URL_SAFE_NO_PAD
165 .decode(&self.signature)
166 .map_err(|_| ProtocolError::InvalidSignature)?;
167
168 let mut message = Vec::new();
169 message.extend_from_slice(token.session_id.as_bytes());
170 message.extend_from_slice(token.short_code.as_bytes());
171 message.extend_from_slice(&initiator_ecdh_bytes);
172 message.extend_from_slice(&device_ecdh_bytes);
173
174 let key = build_keri_public_key(self.curve.into(), &device_signing_bytes)?;
175 key.verify_signature(&message, &signature_bytes)
176 .map_err(|_| ProtocolError::InvalidSignature)
177 }
178
179 pub fn device_ephemeral_pubkey_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
181 URL_SAFE_NO_PAD
182 .decode(&self.device_ephemeral_pubkey)
183 .map_err(|_| ProtocolError::InvalidSignature)
184 }
185
186 pub fn device_signing_pubkey_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
187 URL_SAFE_NO_PAD
188 .decode(&self.device_signing_pubkey)
189 .map_err(|_| ProtocolError::InvalidSignature)
190 }
191}
192
193fn build_keri_public_key(curve: CurveType, bytes: &[u8]) -> Result<KeriPublicKey, ProtocolError> {
197 match curve {
198 CurveType::Ed25519 => {
199 let arr: [u8; 32] = bytes.try_into().map_err(|_| {
200 ProtocolError::KeyExchangeFailed(format!(
201 "Ed25519 pubkey must be 32 bytes, got {}",
202 bytes.len()
203 ))
204 })?;
205 KeriPublicKey::ed25519(&arr).map_err(|e| {
206 ProtocolError::KeyExchangeFailed(format!("Ed25519 pubkey invalid: {e}"))
207 })
208 }
209 CurveType::P256 => {
210 let arr: [u8; 33] = bytes.try_into().map_err(|_| {
211 ProtocolError::KeyExchangeFailed(format!(
212 "P-256 compressed pubkey must be 33 bytes, got {}",
213 bytes.len()
214 ))
215 })?;
216 Ok(KeriPublicKey::P256 {
217 key: arr,
218 transferable: true,
219 })
220 }
221 }
222}
223
224fn typed_sign_sync(seed: &TypedSeed, message: &[u8]) -> Result<Vec<u8>, ProtocolError> {
229 auths_crypto::typed_sign(seed, message).map_err(|e| ProtocolError::KeyGenFailed(format!("{e}")))
230}
231
232#[cfg(test)]
237fn generate_test_keypair() -> Result<(TypedSeed, Vec<u8>), ProtocolError> {
238 use p256::ecdsa::SigningKey;
239 use p256::elliptic_curve::rand_core::OsRng as P256Rng;
240 use p256::pkcs8::EncodePrivateKey;
241
242 let sk = SigningKey::random(&mut P256Rng);
243 #[allow(clippy::disallowed_methods)] let pkcs8 = sk
245 .to_pkcs8_der()
246 .map_err(|e| ProtocolError::KeyGenFailed(format!("{e}")))?;
247 let parsed = auths_crypto::parse_key_material(pkcs8.as_bytes())
248 .map_err(|e| ProtocolError::KeyGenFailed(format!("{e}")))?;
249 Ok((parsed.seed, parsed.public_key))
250}
251
252#[cfg(test)]
253#[allow(clippy::disallowed_methods)]
254mod tests {
255 use super::*;
256 use crate::token::PairingToken;
257 use auths_keri::Capability;
258
259 fn make_token() -> crate::token::PairingSession {
260 PairingToken::generate(
261 chrono::Utc::now(),
262 "did:keri:test123".to_string(),
263 "http://localhost:3000".to_string(),
264 vec![Capability::sign_commit()],
265 )
266 .unwrap()
267 }
268
269 #[test]
270 fn test_create_and_verify_response_p256() {
271 let now = chrono::Utc::now();
272 let session = make_token();
273 let (seed, pubkey) = generate_test_keypair().unwrap();
274
275 let (response, _shared_secret) = PairingResponse::create(
276 now,
277 &session.token,
278 &seed,
279 &pubkey,
280 "did:key:zDnaTest".to_string(),
281 Some("Test Device".to_string()),
282 )
283 .unwrap();
284
285 assert_eq!(response.curve, CurveTag::P256);
286 assert!(response.verify(now, &session.token).is_ok());
287 }
288
289 #[test]
290 fn test_create_and_verify_response_ed25519() {
291 use ring::rand::SystemRandom;
292 use ring::signature::Ed25519KeyPair;
293
294 let now = chrono::Utc::now();
295 let session = make_token();
296 let rng = SystemRandom::new();
297 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
298 let parsed = auths_crypto::parse_key_material(pkcs8.as_ref()).unwrap();
299
300 let (response, _shared_secret) = PairingResponse::create(
301 now,
302 &session.token,
303 &parsed.seed,
304 &parsed.public_key,
305 "did:key:z6MkTest".to_string(),
306 Some("Test Device".to_string()),
307 )
308 .unwrap();
309
310 assert_eq!(response.curve, CurveTag::Ed25519);
311 assert!(response.verify(now, &session.token).is_ok());
312 }
313
314 #[test]
315 fn test_expired_token_rejected() {
316 use chrono::Duration;
317
318 let now = chrono::Utc::now();
319 let session = PairingToken::generate_with_expiry(
320 now,
321 "did:keri:test".to_string(),
322 "http://localhost:3000".to_string(),
323 vec![],
324 Duration::seconds(-1),
325 )
326 .unwrap();
327 let (seed, pubkey) = generate_test_keypair().unwrap();
328
329 let result = PairingResponse::create(
330 now,
331 &session.token,
332 &seed,
333 &pubkey,
334 "did:key:zDnaTest".to_string(),
335 None,
336 );
337 assert!(matches!(result, Err(ProtocolError::Expired)));
338 }
339
340 #[test]
341 fn test_tampered_signature_rejected() {
342 let now = chrono::Utc::now();
343 let session = make_token();
344 let (seed, pubkey) = generate_test_keypair().unwrap();
345
346 let (mut response, _) = PairingResponse::create(
347 now,
348 &session.token,
349 &seed,
350 &pubkey,
351 "did:key:zDnaTest".to_string(),
352 None,
353 )
354 .unwrap();
355
356 let mut sig_bytes = URL_SAFE_NO_PAD.decode(&response.signature).unwrap();
357 sig_bytes[0] ^= 0xFF;
358 response.signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
359
360 let result = response.verify(now, &session.token);
361 assert!(matches!(result, Err(ProtocolError::InvalidSignature)));
362 }
363
364 #[test]
365 fn test_curve_length_mismatch_rejected() {
366 let now = chrono::Utc::now();
370 let session = make_token();
371 let (seed, _good_pubkey) = generate_test_keypair().unwrap();
372 let wrong_len_pubkey = vec![0u8; 32];
373
374 let result = PairingResponse::create(
375 now,
376 &session.token,
377 &seed,
378 &wrong_len_pubkey,
379 "did:key:zDnaTest".to_string(),
380 None,
381 );
382 assert!(matches!(result, Err(ProtocolError::KeyExchangeFailed(_))));
383 }
384
385 #[test]
386 fn test_shared_secret_matches() {
387 let now = chrono::Utc::now();
388 let mut session = make_token();
389 let (seed, pubkey) = generate_test_keypair().unwrap();
390
391 let (response, responder_secret) = PairingResponse::create(
392 now,
393 &session.token,
394 &seed,
395 &pubkey,
396 "did:key:zDnaTest".to_string(),
397 None,
398 )
399 .unwrap();
400
401 let device_ecdh_bytes = response.device_ephemeral_pubkey_bytes().unwrap();
402 let initiator_secret = session.complete_exchange(&device_ecdh_bytes).unwrap();
403
404 assert_eq!(*initiator_secret, *responder_secret);
405 }
406
407 #[test]
408 fn test_session_consumed_prevents_reuse() {
409 let now = chrono::Utc::now();
410 let mut session = make_token();
411 let (seed, pubkey) = generate_test_keypair().unwrap();
412
413 let (response, _) = PairingResponse::create(
414 now,
415 &session.token,
416 &seed,
417 &pubkey,
418 "did:key:zDnaTest".to_string(),
419 None,
420 )
421 .unwrap();
422
423 let device_ecdh_bytes = response.device_ephemeral_pubkey_bytes().unwrap();
424
425 assert!(session.complete_exchange(&device_ecdh_bytes).is_ok());
426
427 let result = session.complete_exchange(&device_ecdh_bytes);
428 assert!(matches!(result, Err(ProtocolError::SessionConsumed)));
429 }
430}