//! MLS KeyPackage generation and TLS serialisation. //! //! # Ciphersuite //! //! `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` (ciphersuite ID `0x0001`). //! This is the RECOMMENDED ciphersuite from RFC 9420 §17.1. //! //! # Single-use semantics //! //! Per RFC 9420 §10.1, each KeyPackage MUST be used at most once. The //! Authentication Service enforces this by atomically removing a package on //! fetch. //! //! # Wire format //! //! KeyPackages are TLS-encoded using `tls_codec` (same version as openmls). //! The resulting bytes are opaque to the quicprochat transport layer. use openmls::prelude::{ BasicCredential, Ciphersuite, CredentialWithKey, KeyPackage, KeyPackageIn, }; use openmls_rust_crypto::OpenMlsRustCrypto; use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait}; use sha2::{Digest, Sha256}; use crate::{error::CoreError, identity::IdentityKeypair}; /// The MLS ciphersuite used throughout quicprochat (RFC 9420 §17.1). pub const ALLOWED_CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; /// Wire value of the allowed ciphersuite (KeyPackage TLS encoding: version 2B, ciphersuite 2B). const ALLOWED_CIPHERSUITE_WIRE: u16 = 0x0001; const CIPHERSUITE: Ciphersuite = ALLOWED_CIPHERSUITE; /// Validates that the KeyPackage bytes use an allowed ciphersuite (Phase 2: ciphersuite allowlist). /// /// Parses the TLS-encoded KeyPackage and rejects if the ciphersuite is not /// `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`. Does not verify signatures; /// the server uses this only to enforce policy before storing. pub fn validate_keypackage_ciphersuite(bytes: &[u8]) -> Result<(), CoreError> { if bytes.len() < 4 { return Err(CoreError::Mls("KeyPackage too short for version+ciphersuite".into())); } let cs_wire = u16::from_be_bytes([bytes[2], bytes[3]]); if cs_wire != ALLOWED_CIPHERSUITE_WIRE { return Err(CoreError::Mls(format!( "KeyPackage ciphersuite {:#06x} not in allowlist (only {:#06x} allowed)", cs_wire, ALLOWED_CIPHERSUITE_WIRE ))); } // Optionally confirm full parse so we don't accept garbage that happens to have 0x0001 at offset 2. let mut cursor = bytes; let _kp = KeyPackageIn::tls_deserialize(&mut cursor) .map_err(|e| CoreError::Mls(format!("KeyPackage parse: {e:?}")))?; Ok(()) } /// Generate a fresh MLS KeyPackage for `identity` and serialise it. /// /// # Returns /// /// `(tls_bytes, sha256_fingerprint)` where: /// - `tls_bytes` is the TLS-encoded KeyPackage blob, suitable for uploading. /// - `sha256_fingerprint` is the SHA-256 digest of `tls_bytes` for tamper detection. /// /// # Errors /// /// Returns [`CoreError::Mls`] if openmls fails to create the KeyPackage or if /// TLS serialisation fails. pub fn generate_key_package(identity: &IdentityKeypair) -> Result<(Vec, Vec), CoreError> { let backend = OpenMlsRustCrypto::default(); // Build a BasicCredential using the raw Ed25519 public key bytes as the // MLS identity. Per RFC 9420, any byte string may serve as the identity. let credential: openmls::prelude::Credential = BasicCredential::new(identity.public_key_bytes().to_vec()).into(); // The `signature_key` in CredentialWithKey is the Ed25519 public key that // will be used to verify the KeyPackage's leaf node signature. // `SignaturePublicKey` implements `From>`. let credential_with_key = CredentialWithKey { credential, signature_key: identity.public_key_bytes().to_vec().into(), }; // `IdentityKeypair` implements `openmls_traits::signatures::Signer` // so it can be passed directly to the builder. let key_package_bundle = KeyPackage::builder() .build(CIPHERSUITE, &backend, identity, credential_with_key) .map_err(|e| CoreError::Mls(format!("{e:?}")))?; // TLS-encode the KeyPackage. let tls_bytes = key_package_bundle .key_package() .tls_serialize_detached() .map_err(|e| CoreError::Mls(format!("{e:?}")))?; let fingerprint: Vec = Sha256::digest(&tls_bytes).to_vec(); Ok((tls_bytes, fingerprint)) }