//! Ed25519 identity keypair for MLS credentials and AS registration. //! //! The [`IdentityKeypair`] is the long-term identity key embedded in MLS //! `BasicCredential`s. It is used for signing MLS messages and as the //! indexing key for the Authentication Service. //! //! # Zeroize //! //! The 32-byte private seed is stored as `Zeroizing<[u8; 32]>`, which zeroes //! the bytes on drop. `[u8; 32]` is `Copy + Default` and satisfies zeroize's //! `DefaultIsZeroes` constraint, avoiding a conflict with ed25519-dalek's //! `SigningKey` zeroize impl. //! //! # Fingerprint //! //! A 32-byte SHA-256 digest of the raw public key bytes is used as a compact, //! collision-resistant identifier for logging. use ed25519_dalek::{Signer as DalekSigner, SigningKey, VerifyingKey}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use zeroize::Zeroizing; /// An Ed25519 identity keypair. /// /// Created with [`IdentityKeypair::generate`]. The private signing key seed /// is zeroed when this struct is dropped. pub struct IdentityKeypair { /// Raw 32-byte private seed — zeroized on drop. /// /// Stored as bytes rather than `SigningKey` to satisfy zeroize's /// `DefaultIsZeroes` bound on `Zeroizing`. seed: Zeroizing<[u8; 32]>, /// Corresponding 32-byte public verifying key. verifying: VerifyingKey, } impl IdentityKeypair { /// Recreate an identity keypair from a 32-byte seed. pub fn from_seed(seed: [u8; 32]) -> Self { let signing = SigningKey::from_bytes(&seed); let verifying = signing.verifying_key(); Self { seed: Zeroizing::new(seed), verifying, } } /// Return the raw 32-byte private seed (for persistence). /// /// The returned value is wrapped in [`Zeroizing`] so it is securely /// erased when dropped, preventing the seed from lingering in memory. pub fn seed_bytes(&self) -> Zeroizing<[u8; 32]> { Zeroizing::new(*self.seed) } } impl IdentityKeypair { /// Generate a fresh random Ed25519 identity keypair. pub fn generate() -> Self { use rand::rngs::OsRng; let signing = SigningKey::generate(&mut OsRng); let verifying = signing.verifying_key(); let seed = Zeroizing::new(signing.to_bytes()); Self { seed, verifying } } /// Return the raw 32-byte Ed25519 public key. /// /// This is the byte array used as `identityKey` in `auth.capnp` calls. pub fn public_key_bytes(&self) -> [u8; 32] { self.verifying.to_bytes() } /// Return the SHA-256 fingerprint of the public key (32 bytes). pub fn fingerprint(&self) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(self.verifying.to_bytes()); hasher.finalize().into() } /// Reconstruct the `SigningKey` from the stored seed bytes. fn signing_key(&self) -> SigningKey { SigningKey::from_bytes(&self.seed) } } /// Implement the openmls `Signer` trait so `IdentityKeypair` can be passed /// directly to `KeyPackage::builder().build(...)` without needing the external /// `openmls_basic_credential` crate. #[cfg(feature = "native")] impl openmls_traits::signatures::Signer for IdentityKeypair { fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { let sk = self.signing_key(); let sig: ed25519_dalek::Signature = sk.sign(payload); Ok(sig.to_bytes().to_vec()) } fn signature_scheme(&self) -> openmls_traits::types::SignatureScheme { openmls_traits::types::SignatureScheme::ED25519 } } impl IdentityKeypair { /// Sign arbitrary bytes with the Ed25519 key and return the 64-byte signature. /// /// Used by sealed sender to sign the inner payload for recipient verification. pub fn sign_raw(&self, payload: &[u8]) -> [u8; 64] { let sk = self.signing_key(); let sig: ed25519_dalek::Signature = sk.sign(payload); sig.to_bytes() } /// Verify an Ed25519 signature over `payload` using the given public key. pub fn verify_raw( public_key: &[u8; 32], payload: &[u8], signature: &[u8; 64], ) -> Result<(), crate::error::CoreError> { use ed25519_dalek::Verifier; let vk = VerifyingKey::from_bytes(public_key) .map_err(|e| crate::error::CoreError::Mls(format!("invalid public key: {e}")))?; let sig = ed25519_dalek::Signature::from_bytes(signature); vk.verify(payload, &sig) .map_err(|e| crate::error::CoreError::Mls(format!("signature verification failed: {e}"))) } } /// Verify a 96-byte delivery proof produced by the server's `build_delivery_proof`. /// /// # Layout /// ```text /// bytes 0..32 — SHA-256(seq_le || recipient_key || timestamp_ms_le) /// bytes 32..96 — Ed25519 signature over those 32 bytes /// ``` /// /// Returns `Ok(true)` when the proof is structurally valid and the signature verifies, /// `Ok(false)` when the proof length is wrong (graceful degradation for old servers), /// or `Err` when the signature is structurally invalid / verification fails. pub fn verify_delivery_proof( server_pubkey: &[u8; 32], proof: &[u8], ) -> Result { if proof.len() != 96 { return Ok(false); } let hash: [u8; 32] = proof[..32].try_into().expect("slice is 32 bytes"); let sig: [u8; 64] = proof[32..96].try_into().expect("slice is 64 bytes"); IdentityKeypair::verify_raw(server_pubkey, &hash, &sig)?; Ok(true) } impl Serialize for IdentityKeypair { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_bytes(&self.seed[..]) } } impl<'de> Deserialize<'de> for IdentityKeypair { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let bytes: Vec = serde::Deserialize::deserialize(deserializer)?; let seed: [u8; 32] = bytes .as_slice() .try_into() .map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?; Ok(IdentityKeypair::from_seed(seed)) } } impl std::fmt::Debug for IdentityKeypair { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fp = self.fingerprint(); f.debug_struct("IdentityKeypair") .field( "fingerprint", &format!("{:02x}{:02x}{:02x}{:02x}…", fp[0], fp[1], fp[2], fp[3]), ) .finish_non_exhaustive() } } #[cfg(test)] #[allow(clippy::unwrap_used)] mod proof_tests { use super::*; use sha2::{Digest, Sha256}; fn make_proof(kp: &IdentityKeypair, seq: u64, recipient_key: &[u8], timestamp_ms: u64) -> Vec { let mut hasher = Sha256::new(); hasher.update(seq.to_le_bytes()); hasher.update(recipient_key); hasher.update(timestamp_ms.to_le_bytes()); let hash: [u8; 32] = hasher.finalize().into(); let sig = kp.sign_raw(&hash); let mut proof = vec![0u8; 96]; proof[..32].copy_from_slice(&hash); proof[32..].copy_from_slice(&sig); proof } #[test] fn verify_valid_proof() { let kp = IdentityKeypair::generate(); let pk = kp.public_key_bytes(); let rk = [0xabu8; 32]; let proof = make_proof(&kp, 42, &rk, 1_700_000_000_000); assert!(verify_delivery_proof(&pk, &proof).unwrap()); } #[test] fn reject_wrong_length() { let kp = IdentityKeypair::generate(); let pk = kp.public_key_bytes(); assert!(!verify_delivery_proof(&pk, &[0u8; 64]).unwrap()); assert!(!verify_delivery_proof(&pk, &[]).unwrap()); assert!(!verify_delivery_proof(&pk, &[0u8; 97]).unwrap()); } #[test] fn reject_tampered_hash() { let kp = IdentityKeypair::generate(); let pk = kp.public_key_bytes(); let rk = [0x01u8; 32]; let mut proof = make_proof(&kp, 1, &rk, 999); proof[0] ^= 0xff; // corrupt the hash bytes assert!(verify_delivery_proof(&pk, &proof).is_err()); } #[test] fn reject_wrong_pubkey() { let kp = IdentityKeypair::generate(); let other = IdentityKeypair::generate(); let pk = other.public_key_bytes(); let rk = [0x02u8; 32]; let proof = make_proof(&kp, 5, &rk, 0); assert!(verify_delivery_proof(&pk, &proof).is_err()); } }