1use crate::{
4 error::{MrvbError, MrvbResult},
5 types::{
6 AssertionClaims, ClassicalKeyPair, HybridSignature, KeyPairSet, MrvbConfig, MrvbMode,
7 SignedAssertion,
8 },
9 MrvbAssertionSigner, MrvbAssertionVerifier,
10};
11use async_trait::async_trait;
12use ed25519_dalek::{Signature, Signer as DalekSigner, SigningKey, Verifier, VerifyingKey};
13use rand::rngs::OsRng;
14
15pub struct Ed25519AssertionSigner {
19 config: MrvbConfig,
20 signing_key: SigningKey,
21 verifying_key: VerifyingKey,
22 keyset: KeyPairSet,
23}
24
25impl Ed25519AssertionSigner {
26 pub fn generate(config: MrvbConfig) -> MrvbResult<Self> {
28 if config.mode != MrvbMode::ClassicalOnly {
29 return Err(MrvbError::InvalidMode(format!(
30 "Ed25519AssertionSigner only supports ClassicalOnly mode, got {:?}",
31 config.mode
32 )));
33 }
34
35 let mut csprng = OsRng;
36 let signing_key = SigningKey::generate(&mut csprng);
37 let verifying_key = signing_key.verifying_key();
38
39 let classical_keypair = ClassicalKeyPair {
40 key_id: format!("ed25519-{}", uuid::Uuid::new_v4()),
41 algorithm: "ed25519".to_string(),
42 private_key: signing_key.to_bytes().to_vec(),
43 public_key: verifying_key.to_bytes().to_vec(),
44 };
45
46 let keyset = KeyPairSet {
47 keyset_id: config.keyset_id.clone(),
48 classical: Some(classical_keypair),
49 pqc: None,
50 created_at: Some(chrono::Utc::now()),
51 rotate_after: None,
52 };
53
54 Ok(Self {
55 config,
56 signing_key,
57 verifying_key,
58 keyset,
59 })
60 }
61
62 pub fn from_keyset(config: MrvbConfig, keyset: KeyPairSet) -> MrvbResult<Self> {
64 if config.mode != MrvbMode::ClassicalOnly {
65 return Err(MrvbError::InvalidMode(format!(
66 "Ed25519AssertionSigner only supports ClassicalOnly mode, got {:?}",
67 config.mode
68 )));
69 }
70
71 let classical = keyset
72 .classical
73 .as_ref()
74 .ok_or(MrvbError::KeysetUnavailable)?;
75
76 if classical.algorithm != "ed25519" {
77 return Err(MrvbError::UnsupportedAlgorithm(format!(
78 "expected ed25519, got {}",
79 classical.algorithm
80 )));
81 }
82
83 if classical.private_key.len() != 32 {
85 return Err(MrvbError::InvalidKeyLength {
86 expected: 32,
87 actual: classical.private_key.len(),
88 });
89 }
90
91 let key_bytes: [u8; 32] = classical.private_key.as_slice().try_into().map_err(|_| {
92 MrvbError::InvalidKeyLength {
93 expected: 32,
94 actual: classical.private_key.len(),
95 }
96 })?;
97
98 let signing_key = SigningKey::from_bytes(&key_bytes);
99 let verifying_key = signing_key.verifying_key();
100
101 Ok(Self {
102 config,
103 signing_key,
104 verifying_key,
105 keyset,
106 })
107 }
108
109 pub fn verifying_key(&self) -> &VerifyingKey {
111 &self.verifying_key
112 }
113
114 pub fn export_keyset(&self) -> &KeyPairSet {
116 &self.keyset
117 }
118
119 pub fn export_public_key_bytes(&self) -> Vec<u8> {
121 self.verifying_key.to_bytes().to_vec()
122 }
123}
124
125#[async_trait]
126impl MrvbAssertionSigner for Ed25519AssertionSigner {
127 async fn sign_assertion(&self, claims: &AssertionClaims) -> MrvbResult<SignedAssertion> {
128 let claims_json = serde_json::to_vec(claims)?;
130
131 let signature: Signature = self.signing_key.sign(&claims_json);
133
134 let hybrid_sig = HybridSignature {
136 classical_sig: Some(signature.to_bytes().to_vec()),
137 pqc_sig: None,
138 alg_classical: Some("ed25519".to_string()),
139 alg_pqc: None,
140 keyset_id: self.config.keyset_id.clone(),
141 mode: MrvbMode::ClassicalOnly,
142 };
143
144 Ok(SignedAssertion {
145 claims: claims.clone(),
146 signature: hybrid_sig,
147 version: "1.0".to_string(),
148 })
149 }
150
151 fn current_keyset_id(&self) -> &str {
152 &self.config.keyset_id
153 }
154
155 fn mode(&self) -> MrvbMode {
156 self.config.mode
157 }
158
159 fn verifier(&self) -> Box<dyn MrvbAssertionVerifier> {
160 Box::new(Ed25519AssertionVerifier {
161 config: self.config.clone(),
162 verifying_key: self.verifying_key,
163 })
164 }
165}
166
167#[derive(Clone)]
171pub struct Ed25519AssertionVerifier {
172 config: MrvbConfig,
173 verifying_key: VerifyingKey,
174}
175
176impl Ed25519AssertionVerifier {
177 pub fn new(config: MrvbConfig, verifying_key: VerifyingKey) -> Self {
179 Self {
180 config,
181 verifying_key,
182 }
183 }
184
185 pub fn from_keyset(config: MrvbConfig, keyset: &KeyPairSet) -> MrvbResult<Self> {
187 if config.mode != MrvbMode::ClassicalOnly {
188 return Err(MrvbError::InvalidMode(format!(
189 "Ed25519AssertionVerifier only supports ClassicalOnly mode, got {:?}",
190 config.mode
191 )));
192 }
193
194 let classical = keyset
195 .classical
196 .as_ref()
197 .ok_or(MrvbError::KeysetUnavailable)?;
198
199 if classical.algorithm != "ed25519" {
200 return Err(MrvbError::UnsupportedAlgorithm(format!(
201 "expected ed25519, got {}",
202 classical.algorithm
203 )));
204 }
205
206 if classical.public_key.len() != 32 {
208 return Err(MrvbError::InvalidKeyLength {
209 expected: 32,
210 actual: classical.public_key.len(),
211 });
212 }
213
214 let key_bytes: [u8; 32] = classical.public_key.as_slice().try_into().map_err(|_| {
215 MrvbError::InvalidKeyLength {
216 expected: 32,
217 actual: classical.public_key.len(),
218 }
219 })?;
220
221 let verifying_key =
222 VerifyingKey::from_bytes(&key_bytes).map_err(|_| MrvbError::InvalidSignature)?;
223
224 Ok(Self {
225 config,
226 verifying_key,
227 })
228 }
229
230 pub fn verifying_key(&self) -> &VerifyingKey {
232 &self.verifying_key
233 }
234}
235
236impl MrvbAssertionVerifier for Ed25519AssertionVerifier {
237 fn verify_assertion(&self, assertion: &SignedAssertion) -> MrvbResult<AssertionClaims> {
238 if assertion.signature.mode != MrvbMode::ClassicalOnly {
240 return Err(MrvbError::InvalidMode(format!(
241 "expected ClassicalOnly mode, got {:?}",
242 assertion.signature.mode
243 )));
244 }
245
246 if assertion.signature.keyset_id != self.config.keyset_id {
248 return Err(MrvbError::VerificationFailed(format!(
249 "keyset ID mismatch: expected {}, got {}",
250 self.config.keyset_id, assertion.signature.keyset_id
251 )));
252 }
253
254 let sig_bytes = assertion
256 .signature
257 .classical_sig
258 .as_ref()
259 .ok_or(MrvbError::InvalidSignature)?;
260
261 let signature =
263 Signature::from_slice(sig_bytes).map_err(|_| MrvbError::InvalidSignature)?;
264
265 let claims_json = serde_json::to_vec(&assertion.claims)?;
267
268 self.verifying_key
270 .verify(&claims_json, &signature)
271 .map_err(|_| {
272 MrvbError::VerificationFailed("signature verification failed".to_string())
273 })?;
274
275 if assertion.claims.is_expired() {
277 return Err(MrvbError::TokenExpired);
278 }
279
280 Ok(assertion.claims.clone())
281 }
282
283 fn keyset_id(&self) -> &str {
284 &self.config.keyset_id
285 }
286
287 fn mode(&self) -> MrvbMode {
288 self.config.mode
289 }
290}
291
292impl std::fmt::Debug for Ed25519AssertionVerifier {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("Ed25519AssertionVerifier")
295 .field("config", &self.config)
296 .field("verifying_key", &"<redacted>")
297 .finish()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::collections::HashMap;
305
306 #[tokio::test]
307 async fn test_generate_and_sign() {
308 let config = MrvbConfig {
309 mode: MrvbMode::ClassicalOnly,
310 keyset_id: "test-keyset".to_string(),
311 };
312
313 let signer = Ed25519AssertionSigner::generate(config).unwrap();
314
315 let claims = AssertionClaims {
316 session_id: "session_abc".to_string(),
317 user_id: Some("user_123".to_string()),
318 rail: "email".to_string(),
319 verification_level: "high".to_string(),
320 issued_at: chrono::Utc::now(),
321 expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
322 metadata: HashMap::new(),
323 };
324
325 let assertion = signer.sign_assertion(&claims).await.unwrap();
326
327 assert_eq!(assertion.claims.session_id, "session_abc");
328 assert!(assertion.signature.classical_sig.is_some());
329 assert_eq!(assertion.signature.mode, MrvbMode::ClassicalOnly);
330 }
331
332 #[tokio::test]
333 async fn test_sign_and_verify() {
334 let config = MrvbConfig {
335 mode: MrvbMode::ClassicalOnly,
336 keyset_id: "test-keyset".to_string(),
337 };
338
339 let signer = Ed25519AssertionSigner::generate(config).unwrap();
340
341 let claims = AssertionClaims {
342 session_id: "session_xyz".to_string(),
343 user_id: Some("user_456".to_string()),
344 rail: "sms".to_string(),
345 verification_level: "medium".to_string(),
346 issued_at: chrono::Utc::now(),
347 expires_at: chrono::Utc::now() + chrono::Duration::hours(2),
348 metadata: HashMap::new(),
349 };
350
351 let assertion = signer.sign_assertion(&claims).await.unwrap();
352
353 let verifier = signer.verifier();
354 let verified_claims = verifier.verify_assertion(&assertion).unwrap();
355
356 assert_eq!(verified_claims.session_id, claims.session_id);
357 assert_eq!(verified_claims.user_id, claims.user_id);
358 assert_eq!(verified_claims.rail, claims.rail);
359 }
360
361 #[tokio::test]
362 async fn test_corrupted_signature_fails() {
363 let config = MrvbConfig {
364 mode: MrvbMode::ClassicalOnly,
365 keyset_id: "test-keyset".to_string(),
366 };
367
368 let signer = Ed25519AssertionSigner::generate(config).unwrap();
369
370 let claims = AssertionClaims {
371 session_id: "session_corrupted".to_string(),
372 user_id: Some("user_789".to_string()),
373 rail: "webauthn".to_string(),
374 verification_level: "high".to_string(),
375 issued_at: chrono::Utc::now(),
376 expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
377 metadata: HashMap::new(),
378 };
379
380 let mut assertion = signer.sign_assertion(&claims).await.unwrap();
381
382 if let Some(ref mut sig) = assertion.signature.classical_sig {
384 sig[0] ^= 0xFF;
385 }
386
387 let verifier = signer.verifier();
388 let result = verifier.verify_assertion(&assertion);
389
390 assert!(result.is_err());
391 assert!(matches!(
392 result.unwrap_err(),
393 MrvbError::VerificationFailed(_)
394 ));
395 }
396
397 #[tokio::test]
398 async fn test_expired_token_rejected() {
399 let config = MrvbConfig {
400 mode: MrvbMode::ClassicalOnly,
401 keyset_id: "test-keyset".to_string(),
402 };
403
404 let signer = Ed25519AssertionSigner::generate(config).unwrap();
405
406 let claims = AssertionClaims {
407 session_id: "session_expired".to_string(),
408 user_id: Some("user_999".to_string()),
409 rail: "email".to_string(),
410 verification_level: "high".to_string(),
411 issued_at: chrono::Utc::now() - chrono::Duration::hours(2),
412 expires_at: chrono::Utc::now() - chrono::Duration::hours(1),
413 metadata: HashMap::new(),
414 };
415
416 let assertion = signer.sign_assertion(&claims).await.unwrap();
417
418 let verifier = signer.verifier();
419 let result = verifier.verify_assertion(&assertion);
420
421 assert!(result.is_err());
422 assert!(matches!(result.unwrap_err(), MrvbError::TokenExpired));
423 }
424
425 #[tokio::test]
426 async fn test_keyset_export_and_load() {
427 let config = MrvbConfig {
428 mode: MrvbMode::ClassicalOnly,
429 keyset_id: "export-test".to_string(),
430 };
431
432 let signer1 = Ed25519AssertionSigner::generate(config.clone()).unwrap();
433 let keyset = signer1.export_keyset().clone();
434
435 let signer2 = Ed25519AssertionSigner::from_keyset(config, keyset).unwrap();
437
438 let claims = AssertionClaims {
440 session_id: "session_load_test".to_string(),
441 user_id: Some("user_load".to_string()),
442 rail: "push".to_string(),
443 verification_level: "medium".to_string(),
444 issued_at: chrono::Utc::now(),
445 expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
446 metadata: HashMap::new(),
447 };
448
449 let assertion = signer1.sign_assertion(&claims).await.unwrap();
450 let verifier = signer2.verifier();
451 let verified_claims = verifier.verify_assertion(&assertion).unwrap();
452
453 assert_eq!(verified_claims.session_id, claims.session_id);
454 }
455
456 #[test]
457 fn test_claims_validation() {
458 let now = chrono::Utc::now();
459
460 let valid_claims = AssertionClaims {
462 session_id: "session_valid".to_string(),
463 user_id: None,
464 rail: "email".to_string(),
465 verification_level: "high".to_string(),
466 issued_at: now - chrono::Duration::minutes(5),
467 expires_at: now + chrono::Duration::hours(1),
468 metadata: HashMap::new(),
469 };
470 assert!(valid_claims.is_valid());
471 assert!(!valid_claims.is_expired());
472
473 let expired_claims = AssertionClaims {
475 session_id: "session_expired".to_string(),
476 user_id: None,
477 rail: "email".to_string(),
478 verification_level: "high".to_string(),
479 issued_at: now - chrono::Duration::hours(2),
480 expires_at: now - chrono::Duration::hours(1),
481 metadata: HashMap::new(),
482 };
483 assert!(!expired_claims.is_valid());
484 assert!(expired_claims.is_expired());
485 }
486}