Skip to main content

uvb_mrvb/
ed25519_signer.rs

1//! Ed25519 assertion signing and verification implementation.
2
3use 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
15/// Ed25519 assertion signer.
16///
17/// This implementation signs MRVB assertion claims using Ed25519.
18pub struct Ed25519AssertionSigner {
19    config: MrvbConfig,
20    signing_key: SigningKey,
21    verifying_key: VerifyingKey,
22    keyset: KeyPairSet,
23}
24
25impl Ed25519AssertionSigner {
26    /// Generate a new Ed25519 keypair for signing.
27    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    /// Load an Ed25519 signer from an existing keypair.
63    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        // Ed25519 private key is 32 bytes
84        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    /// Get the public verifying key.
110    pub fn verifying_key(&self) -> &VerifyingKey {
111        &self.verifying_key
112    }
113
114    /// Export the keyset for storage or transmission.
115    pub fn export_keyset(&self) -> &KeyPairSet {
116        &self.keyset
117    }
118
119    /// Export the public key as bytes.
120    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        // Serialize claims to JSON for signing
129        let claims_json = serde_json::to_vec(claims)?;
130
131        // Sign the claims
132        let signature: Signature = self.signing_key.sign(&claims_json);
133
134        // Create hybrid signature structure (only classical component)
135        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/// Ed25519 assertion verifier.
168///
169/// This implementation verifies MRVB assertion claims using Ed25519.
170#[derive(Clone)]
171pub struct Ed25519AssertionVerifier {
172    config: MrvbConfig,
173    verifying_key: VerifyingKey,
174}
175
176impl Ed25519AssertionVerifier {
177    /// Create a new verifier from a public key.
178    pub fn new(config: MrvbConfig, verifying_key: VerifyingKey) -> Self {
179        Self {
180            config,
181            verifying_key,
182        }
183    }
184
185    /// Create a verifier from a keyset.
186    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        // Ed25519 public key is 32 bytes
207        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    /// Get the public verifying key.
231    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        // Check if signature mode matches
239        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        // Check keyset ID
247        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        // Extract classical signature
255        let sig_bytes = assertion
256            .signature
257            .classical_sig
258            .as_ref()
259            .ok_or(MrvbError::InvalidSignature)?;
260
261        // Parse signature
262        let signature =
263            Signature::from_slice(sig_bytes).map_err(|_| MrvbError::InvalidSignature)?;
264
265        // Serialize claims for verification
266        let claims_json = serde_json::to_vec(&assertion.claims)?;
267
268        // Verify signature
269        self.verifying_key
270            .verify(&claims_json, &signature)
271            .map_err(|_| {
272                MrvbError::VerificationFailed("signature verification failed".to_string())
273            })?;
274
275        // Check expiration
276        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        // Corrupt the signature
383        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        // Load from keyset
436        let signer2 = Ed25519AssertionSigner::from_keyset(config, keyset).unwrap();
437
438        // Sign with one, verify with the other
439        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        // Valid claims
461        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        // Expired claims
474        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}