1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
2use chrono::{DateTime, Utc};
3use rand::rngs::OsRng;
4use ring::signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey};
5use serde::{Deserialize, Serialize};
6use x25519_dalek::{EphemeralSecret, PublicKey};
7use zeroize::Zeroizing;
8
9use auths_crypto::SecureSeed;
10
11use crate::error::ProtocolError;
12use crate::token::PairingToken;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PairingResponse {
17 pub short_code: String,
18 pub device_x25519_pubkey: String,
19 pub device_signing_pubkey: String,
20 pub device_did: String,
21 pub signature: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub device_name: Option<String>,
24}
25
26impl PairingResponse {
27 pub fn create(
37 now: DateTime<Utc>,
38 token: &PairingToken,
39 device_seed: &SecureSeed,
40 device_pubkey: &[u8; 32],
41 device_did: String,
42 device_name: Option<String>,
43 ) -> Result<(Self, Zeroizing<[u8; 32]>), ProtocolError> {
44 if token.is_expired(now) {
45 return Err(ProtocolError::Expired);
46 }
47
48 let device_x25519_secret = EphemeralSecret::random_from_rng(OsRng);
49 let device_x25519_public = PublicKey::from(&device_x25519_secret);
50
51 let initiator_x25519_bytes = token.ephemeral_pubkey_bytes()?;
52 let initiator_x25519 = PublicKey::from(initiator_x25519_bytes);
53
54 let shared = device_x25519_secret.diffie_hellman(&initiator_x25519);
55 let shared_secret = Zeroizing::new(*shared.as_bytes());
56
57 let device_signing_pubkey = URL_SAFE_NO_PAD.encode(device_pubkey);
58 let device_x25519_pubkey_str = URL_SAFE_NO_PAD.encode(device_x25519_public.as_bytes());
59
60 let mut message = Vec::new();
61 message.extend_from_slice(token.short_code.as_bytes());
62 message.extend_from_slice(&initiator_x25519_bytes);
63 message.extend_from_slice(device_x25519_public.as_bytes());
64
65 let sig_bytes = sign_ed25519_sync(device_seed, &message)?;
67 let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
68
69 let response = PairingResponse {
70 short_code: token.short_code.clone(),
71 device_x25519_pubkey: device_x25519_pubkey_str,
72 device_signing_pubkey,
73 device_did,
74 signature,
75 device_name,
76 };
77
78 Ok((response, shared_secret))
79 }
80
81 pub fn verify(&self, now: DateTime<Utc>, token: &PairingToken) -> Result<(), ProtocolError> {
87 if token.is_expired(now) {
88 return Err(ProtocolError::Expired);
89 }
90
91 let initiator_x25519_bytes = token.ephemeral_pubkey_bytes()?;
92 let device_x25519_bytes = self.device_x25519_pubkey_bytes()?;
93 let device_signing_bytes = self.device_signing_pubkey_bytes()?;
94 let signature_bytes = URL_SAFE_NO_PAD
95 .decode(&self.signature)
96 .map_err(|_| ProtocolError::InvalidSignature)?;
97
98 let mut message = Vec::new();
99 message.extend_from_slice(token.short_code.as_bytes());
100 message.extend_from_slice(&initiator_x25519_bytes);
101 message.extend_from_slice(&device_x25519_bytes);
102
103 let peer_public_key = UnparsedPublicKey::new(&ED25519, &device_signing_bytes);
104 peer_public_key
105 .verify(&message, &signature_bytes)
106 .map_err(|_| ProtocolError::InvalidSignature)?;
107
108 Ok(())
109 }
110
111 pub fn device_x25519_pubkey_bytes(&self) -> Result<[u8; 32], ProtocolError> {
112 let bytes = URL_SAFE_NO_PAD
113 .decode(&self.device_x25519_pubkey)
114 .map_err(|_| ProtocolError::InvalidSignature)?;
115 bytes.try_into().map_err(|_| {
116 ProtocolError::KeyExchangeFailed("Invalid X25519 pubkey length".to_string())
117 })
118 }
119
120 pub fn device_signing_pubkey_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
121 URL_SAFE_NO_PAD
122 .decode(&self.device_signing_pubkey)
123 .map_err(|_| ProtocolError::InvalidSignature)
124 }
125}
126
127fn sign_ed25519_sync(seed: &SecureSeed, message: &[u8]) -> Result<Vec<u8>, ProtocolError> {
129 let keypair = Ed25519KeyPair::from_seed_unchecked(seed.as_bytes())
130 .map_err(|e| ProtocolError::KeyGenFailed(format!("{e}")))?;
131 Ok(keypair.sign(message).as_ref().to_vec())
132}
133
134#[cfg(test)]
136fn generate_ed25519_keypair_sync() -> Result<(SecureSeed, [u8; 32]), ProtocolError> {
137 use ring::rand::SystemRandom;
138 use ring::signature::KeyPair;
139
140 let rng = SystemRandom::new();
141 let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng)
142 .map_err(|_| ProtocolError::KeyGenFailed("Key generation failed".to_string()))?;
143 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref())
144 .map_err(|e| ProtocolError::KeyGenFailed(format!("{e}")))?;
145
146 let public_key: [u8; 32] = keypair
147 .public_key()
148 .as_ref()
149 .try_into()
150 .map_err(|_| ProtocolError::KeyGenFailed("Public key not 32 bytes".to_string()))?;
151
152 let pkcs8_bytes = pkcs8_doc.as_ref();
154 let seed: [u8; 32] = pkcs8_bytes[16..48]
155 .try_into()
156 .map_err(|_| ProtocolError::KeyGenFailed("Seed extraction failed".to_string()))?;
157
158 Ok((SecureSeed::new(seed), public_key))
159}
160
161#[cfg(test)]
162#[allow(clippy::disallowed_methods)]
163mod tests {
164 use super::*;
165 use crate::token::PairingToken;
166
167 fn make_token() -> crate::token::PairingSession {
168 PairingToken::generate(
169 chrono::Utc::now(),
170 "did:keri:test123".to_string(),
171 "http://localhost:3000".to_string(),
172 vec!["sign_commit".to_string()],
173 )
174 .unwrap()
175 }
176
177 #[test]
178 fn test_create_and_verify_response() {
179 let now = chrono::Utc::now();
180 let session = make_token();
181 let (seed, pubkey) = generate_ed25519_keypair_sync().unwrap();
182
183 let (response, _shared_secret) = PairingResponse::create(
184 now,
185 &session.token,
186 &seed,
187 &pubkey,
188 "did:key:z6MkTest".to_string(),
189 Some("Test Device".to_string()),
190 )
191 .unwrap();
192
193 assert!(response.verify(now, &session.token).is_ok());
194 }
195
196 #[test]
197 fn test_expired_token_rejected() {
198 use chrono::Duration;
199
200 let now = chrono::Utc::now();
201 let session = PairingToken::generate_with_expiry(
202 now,
203 "did:keri:test".to_string(),
204 "http://localhost:3000".to_string(),
205 vec![],
206 Duration::seconds(-1),
207 )
208 .unwrap();
209 let (seed, pubkey) = generate_ed25519_keypair_sync().unwrap();
210
211 let result = PairingResponse::create(
212 now,
213 &session.token,
214 &seed,
215 &pubkey,
216 "did:key:z6MkTest".to_string(),
217 None,
218 );
219 assert!(matches!(result, Err(ProtocolError::Expired)));
220 }
221
222 #[test]
223 fn test_tampered_signature_rejected() {
224 let now = chrono::Utc::now();
225 let session = make_token();
226 let (seed, pubkey) = generate_ed25519_keypair_sync().unwrap();
227
228 let (mut response, _) = PairingResponse::create(
229 now,
230 &session.token,
231 &seed,
232 &pubkey,
233 "did:key:z6MkTest".to_string(),
234 None,
235 )
236 .unwrap();
237
238 let mut sig_bytes = URL_SAFE_NO_PAD.decode(&response.signature).unwrap();
239 sig_bytes[0] ^= 0xFF;
240 response.signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
241
242 let result = response.verify(now, &session.token);
243 assert!(matches!(result, Err(ProtocolError::InvalidSignature)));
244 }
245
246 #[test]
247 fn test_shared_secret_matches() {
248 let now = chrono::Utc::now();
249 let mut session = make_token();
250 let (seed, pubkey) = generate_ed25519_keypair_sync().unwrap();
251
252 let (response, responder_secret) = PairingResponse::create(
253 now,
254 &session.token,
255 &seed,
256 &pubkey,
257 "did:key:z6MkTest".to_string(),
258 None,
259 )
260 .unwrap();
261
262 let device_x25519_bytes = response.device_x25519_pubkey_bytes().unwrap();
263 let initiator_secret = session.complete_exchange(&device_x25519_bytes).unwrap();
264
265 assert_eq!(*initiator_secret, *responder_secret);
266 }
267
268 #[test]
269 fn test_session_consumed_prevents_reuse() {
270 let now = chrono::Utc::now();
271 let mut session = make_token();
272 let (seed, pubkey) = generate_ed25519_keypair_sync().unwrap();
273
274 let (response, _) = PairingResponse::create(
275 now,
276 &session.token,
277 &seed,
278 &pubkey,
279 "did:key:z6MkTest".to_string(),
280 None,
281 )
282 .unwrap();
283
284 let device_x25519_bytes = response.device_x25519_pubkey_bytes().unwrap();
285
286 assert!(session.complete_exchange(&device_x25519_bytes).is_ok());
287
288 let result = session.complete_exchange(&device_x25519_bytes);
289 assert!(matches!(result, Err(ProtocolError::SessionConsumed)));
290 }
291}