//! 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 openmls_traits::signatures::Signer; use openmls_traits::types::{Error as MlsError, SignatureScheme}; 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). pub fn seed_bytes(&self) -> [u8; 32] { *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. impl Signer for IdentityKeypair { fn sign(&self, payload: &[u8]) -> Result, MlsError> { let sk = self.signing_key(); let sig: ed25519_dalek::Signature = sk.sign(payload); Ok(sig.to_bytes().to_vec()) } fn signature_scheme(&self) -> SignatureScheme { 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}"))) } } 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() } }