use crate::{
error::{MrvbError, MrvbResult},
types::{
AssertionClaims, ClassicalKeyPair, HybridSignature, KeyPairSet, MrvbConfig, MrvbMode,
SignedAssertion,
},
MrvbAssertionSigner, MrvbAssertionVerifier,
};
use async_trait::async_trait;
use ed25519_dalek::{Signature, Signer as DalekSigner, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;
pub struct Ed25519AssertionSigner {
config: MrvbConfig,
signing_key: SigningKey,
verifying_key: VerifyingKey,
keyset: KeyPairSet,
}
impl Ed25519AssertionSigner {
pub fn generate(config: MrvbConfig) -> MrvbResult<Self> {
if config.mode != MrvbMode::ClassicalOnly {
return Err(MrvbError::InvalidMode(format!(
"Ed25519AssertionSigner only supports ClassicalOnly mode, got {:?}",
config.mode
)));
}
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let classical_keypair = ClassicalKeyPair {
key_id: format!("ed25519-{}", uuid::Uuid::new_v4()),
algorithm: "ed25519".to_string(),
private_key: signing_key.to_bytes().to_vec(),
public_key: verifying_key.to_bytes().to_vec(),
};
let keyset = KeyPairSet {
keyset_id: config.keyset_id.clone(),
classical: Some(classical_keypair),
pqc: None,
created_at: Some(chrono::Utc::now()),
rotate_after: None,
};
Ok(Self {
config,
signing_key,
verifying_key,
keyset,
})
}
pub fn from_keyset(config: MrvbConfig, keyset: KeyPairSet) -> MrvbResult<Self> {
if config.mode != MrvbMode::ClassicalOnly {
return Err(MrvbError::InvalidMode(format!(
"Ed25519AssertionSigner only supports ClassicalOnly mode, got {:?}",
config.mode
)));
}
let classical = keyset
.classical
.as_ref()
.ok_or(MrvbError::KeysetUnavailable)?;
if classical.algorithm != "ed25519" {
return Err(MrvbError::UnsupportedAlgorithm(format!(
"expected ed25519, got {}",
classical.algorithm
)));
}
if classical.private_key.len() != 32 {
return Err(MrvbError::InvalidKeyLength {
expected: 32,
actual: classical.private_key.len(),
});
}
let key_bytes: [u8; 32] = classical.private_key.as_slice().try_into().map_err(|_| {
MrvbError::InvalidKeyLength {
expected: 32,
actual: classical.private_key.len(),
}
})?;
let signing_key = SigningKey::from_bytes(&key_bytes);
let verifying_key = signing_key.verifying_key();
Ok(Self {
config,
signing_key,
verifying_key,
keyset,
})
}
pub fn verifying_key(&self) -> &VerifyingKey {
&self.verifying_key
}
pub fn export_keyset(&self) -> &KeyPairSet {
&self.keyset
}
pub fn export_public_key_bytes(&self) -> Vec<u8> {
self.verifying_key.to_bytes().to_vec()
}
}
#[async_trait]
impl MrvbAssertionSigner for Ed25519AssertionSigner {
async fn sign_assertion(&self, claims: &AssertionClaims) -> MrvbResult<SignedAssertion> {
let claims_json = serde_json::to_vec(claims)?;
let signature: Signature = self.signing_key.sign(&claims_json);
let hybrid_sig = HybridSignature {
classical_sig: Some(signature.to_bytes().to_vec()),
pqc_sig: None,
alg_classical: Some("ed25519".to_string()),
alg_pqc: None,
keyset_id: self.config.keyset_id.clone(),
mode: MrvbMode::ClassicalOnly,
};
Ok(SignedAssertion {
claims: claims.clone(),
signature: hybrid_sig,
version: "1.0".to_string(),
})
}
fn current_keyset_id(&self) -> &str {
&self.config.keyset_id
}
fn mode(&self) -> MrvbMode {
self.config.mode
}
fn verifier(&self) -> Box<dyn MrvbAssertionVerifier> {
Box::new(Ed25519AssertionVerifier {
config: self.config.clone(),
verifying_key: self.verifying_key,
})
}
}
#[derive(Clone)]
pub struct Ed25519AssertionVerifier {
config: MrvbConfig,
verifying_key: VerifyingKey,
}
impl Ed25519AssertionVerifier {
pub fn new(config: MrvbConfig, verifying_key: VerifyingKey) -> Self {
Self {
config,
verifying_key,
}
}
pub fn from_keyset(config: MrvbConfig, keyset: &KeyPairSet) -> MrvbResult<Self> {
if config.mode != MrvbMode::ClassicalOnly {
return Err(MrvbError::InvalidMode(format!(
"Ed25519AssertionVerifier only supports ClassicalOnly mode, got {:?}",
config.mode
)));
}
let classical = keyset
.classical
.as_ref()
.ok_or(MrvbError::KeysetUnavailable)?;
if classical.algorithm != "ed25519" {
return Err(MrvbError::UnsupportedAlgorithm(format!(
"expected ed25519, got {}",
classical.algorithm
)));
}
if classical.public_key.len() != 32 {
return Err(MrvbError::InvalidKeyLength {
expected: 32,
actual: classical.public_key.len(),
});
}
let key_bytes: [u8; 32] = classical.public_key.as_slice().try_into().map_err(|_| {
MrvbError::InvalidKeyLength {
expected: 32,
actual: classical.public_key.len(),
}
})?;
let verifying_key =
VerifyingKey::from_bytes(&key_bytes).map_err(|_| MrvbError::InvalidSignature)?;
Ok(Self {
config,
verifying_key,
})
}
pub fn verifying_key(&self) -> &VerifyingKey {
&self.verifying_key
}
}
impl MrvbAssertionVerifier for Ed25519AssertionVerifier {
fn verify_assertion(&self, assertion: &SignedAssertion) -> MrvbResult<AssertionClaims> {
if assertion.signature.mode != MrvbMode::ClassicalOnly {
return Err(MrvbError::InvalidMode(format!(
"expected ClassicalOnly mode, got {:?}",
assertion.signature.mode
)));
}
if assertion.signature.keyset_id != self.config.keyset_id {
return Err(MrvbError::VerificationFailed(format!(
"keyset ID mismatch: expected {}, got {}",
self.config.keyset_id, assertion.signature.keyset_id
)));
}
let sig_bytes = assertion
.signature
.classical_sig
.as_ref()
.ok_or(MrvbError::InvalidSignature)?;
let signature =
Signature::from_slice(sig_bytes).map_err(|_| MrvbError::InvalidSignature)?;
let claims_json = serde_json::to_vec(&assertion.claims)?;
self.verifying_key
.verify(&claims_json, &signature)
.map_err(|_| {
MrvbError::VerificationFailed("signature verification failed".to_string())
})?;
if assertion.claims.is_expired() {
return Err(MrvbError::TokenExpired);
}
Ok(assertion.claims.clone())
}
fn keyset_id(&self) -> &str {
&self.config.keyset_id
}
fn mode(&self) -> MrvbMode {
self.config.mode
}
}
impl std::fmt::Debug for Ed25519AssertionVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ed25519AssertionVerifier")
.field("config", &self.config)
.field("verifying_key", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[tokio::test]
async fn test_generate_and_sign() {
let config = MrvbConfig {
mode: MrvbMode::ClassicalOnly,
keyset_id: "test-keyset".to_string(),
};
let signer = Ed25519AssertionSigner::generate(config).unwrap();
let claims = AssertionClaims {
session_id: "session_abc".to_string(),
user_id: Some("user_123".to_string()),
rail: "email".to_string(),
verification_level: "high".to_string(),
issued_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
metadata: HashMap::new(),
};
let assertion = signer.sign_assertion(&claims).await.unwrap();
assert_eq!(assertion.claims.session_id, "session_abc");
assert!(assertion.signature.classical_sig.is_some());
assert_eq!(assertion.signature.mode, MrvbMode::ClassicalOnly);
}
#[tokio::test]
async fn test_sign_and_verify() {
let config = MrvbConfig {
mode: MrvbMode::ClassicalOnly,
keyset_id: "test-keyset".to_string(),
};
let signer = Ed25519AssertionSigner::generate(config).unwrap();
let claims = AssertionClaims {
session_id: "session_xyz".to_string(),
user_id: Some("user_456".to_string()),
rail: "sms".to_string(),
verification_level: "medium".to_string(),
issued_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(2),
metadata: HashMap::new(),
};
let assertion = signer.sign_assertion(&claims).await.unwrap();
let verifier = signer.verifier();
let verified_claims = verifier.verify_assertion(&assertion).unwrap();
assert_eq!(verified_claims.session_id, claims.session_id);
assert_eq!(verified_claims.user_id, claims.user_id);
assert_eq!(verified_claims.rail, claims.rail);
}
#[tokio::test]
async fn test_corrupted_signature_fails() {
let config = MrvbConfig {
mode: MrvbMode::ClassicalOnly,
keyset_id: "test-keyset".to_string(),
};
let signer = Ed25519AssertionSigner::generate(config).unwrap();
let claims = AssertionClaims {
session_id: "session_corrupted".to_string(),
user_id: Some("user_789".to_string()),
rail: "webauthn".to_string(),
verification_level: "high".to_string(),
issued_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
metadata: HashMap::new(),
};
let mut assertion = signer.sign_assertion(&claims).await.unwrap();
if let Some(ref mut sig) = assertion.signature.classical_sig {
sig[0] ^= 0xFF;
}
let verifier = signer.verifier();
let result = verifier.verify_assertion(&assertion);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
MrvbError::VerificationFailed(_)
));
}
#[tokio::test]
async fn test_expired_token_rejected() {
let config = MrvbConfig {
mode: MrvbMode::ClassicalOnly,
keyset_id: "test-keyset".to_string(),
};
let signer = Ed25519AssertionSigner::generate(config).unwrap();
let claims = AssertionClaims {
session_id: "session_expired".to_string(),
user_id: Some("user_999".to_string()),
rail: "email".to_string(),
verification_level: "high".to_string(),
issued_at: chrono::Utc::now() - chrono::Duration::hours(2),
expires_at: chrono::Utc::now() - chrono::Duration::hours(1),
metadata: HashMap::new(),
};
let assertion = signer.sign_assertion(&claims).await.unwrap();
let verifier = signer.verifier();
let result = verifier.verify_assertion(&assertion);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), MrvbError::TokenExpired));
}
#[tokio::test]
async fn test_keyset_export_and_load() {
let config = MrvbConfig {
mode: MrvbMode::ClassicalOnly,
keyset_id: "export-test".to_string(),
};
let signer1 = Ed25519AssertionSigner::generate(config.clone()).unwrap();
let keyset = signer1.export_keyset().clone();
let signer2 = Ed25519AssertionSigner::from_keyset(config, keyset).unwrap();
let claims = AssertionClaims {
session_id: "session_load_test".to_string(),
user_id: Some("user_load".to_string()),
rail: "push".to_string(),
verification_level: "medium".to_string(),
issued_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
metadata: HashMap::new(),
};
let assertion = signer1.sign_assertion(&claims).await.unwrap();
let verifier = signer2.verifier();
let verified_claims = verifier.verify_assertion(&assertion).unwrap();
assert_eq!(verified_claims.session_id, claims.session_id);
}
#[test]
fn test_claims_validation() {
let now = chrono::Utc::now();
let valid_claims = AssertionClaims {
session_id: "session_valid".to_string(),
user_id: None,
rail: "email".to_string(),
verification_level: "high".to_string(),
issued_at: now - chrono::Duration::minutes(5),
expires_at: now + chrono::Duration::hours(1),
metadata: HashMap::new(),
};
assert!(valid_claims.is_valid());
assert!(!valid_claims.is_expired());
let expired_claims = AssertionClaims {
session_id: "session_expired".to_string(),
user_id: None,
rail: "email".to_string(),
verification_level: "high".to_string(),
issued_at: now - chrono::Duration::hours(2),
expires_at: now - chrono::Duration::hours(1),
metadata: HashMap::new(),
};
assert!(!expired_claims.is_valid());
assert!(expired_claims.is_expired());
}
}