diff --git a/openmls/src/extensions/codec.rs b/openmls/src/extensions/codec.rs index 936ee63fa2..487083b104 100644 --- a/openmls/src/extensions/codec.rs +++ b/openmls/src/extensions/codec.rs @@ -8,7 +8,7 @@ use crate::extensions::{ UnknownExtension, }; -use super::last_resort::LastResortExtension; +use super::{last_resort::LastResortExtension, protected_metadata::ProtectedMetadata}; fn vlbytes_len_len(length: usize) -> usize { if length < 0x40 { @@ -37,6 +37,7 @@ impl Size for Extension { Extension::ExternalPub(e) => e.tls_serialized_len(), Extension::ExternalSenders(e) => e.tls_serialized_len(), Extension::LastResort(e) => e.tls_serialized_len(), + Extension::ProtectedMetadata(e) => e.tls_serialized_len(), Extension::Unknown(_, e) => e.0.len(), }; @@ -69,6 +70,7 @@ impl Serialize for Extension { Extension::ExternalPub(e) => e.tls_serialize(&mut extension_data), Extension::ExternalSenders(e) => e.tls_serialize(&mut extension_data), Extension::LastResort(e) => e.tls_serialize(&mut extension_data), + Extension::ProtectedMetadata(e) => e.tls_serialize(&mut extension_data), Extension::Unknown(_, e) => extension_data .write_all(e.0.as_slice()) .map(|_| e.0.len()) @@ -118,6 +120,9 @@ impl Deserialize for Extension { ExtensionType::LastResort => { Extension::LastResort(LastResortExtension::tls_deserialize(&mut extension_data)?) } + ExtensionType::ProtectedMetadata => Extension::ProtectedMetadata( + ProtectedMetadata::tls_deserialize(&mut extension_data)?, + ), ExtensionType::Unknown(unknown) => { Extension::Unknown(unknown, UnknownExtension(extension_data.to_vec())) } diff --git a/openmls/src/extensions/mod.rs b/openmls/src/extensions/mod.rs index 46d3932cc2..149740903a 100644 --- a/openmls/src/extensions/mod.rs +++ b/openmls/src/extensions/mod.rs @@ -32,6 +32,7 @@ mod codec; mod external_pub_extension; mod external_sender_extension; mod last_resort; +mod protected_metadata; mod ratchet_tree_extension; mod required_capabilities; use errors::*; @@ -49,6 +50,8 @@ pub use last_resort::LastResortExtension; pub use ratchet_tree_extension::RatchetTreeExtension; pub use required_capabilities::RequiredCapabilitiesExtension; +pub use protected_metadata::ProtectedMetadata; + #[cfg(test)] mod test_extensions; @@ -93,6 +96,10 @@ pub enum ExtensionType { /// scenario. LastResort, + /// Protected metadata extension for policies of the group. GroupContext + /// extension + ProtectedMetadata, + /// A currently unknown extension type. Unknown(u16), } @@ -132,6 +139,7 @@ impl From for ExtensionType { 4 => ExtensionType::ExternalPub, 5 => ExtensionType::ExternalSenders, 10 => ExtensionType::LastResort, + 11 => ExtensionType::ProtectedMetadata, unknown => ExtensionType::Unknown(unknown), } } @@ -146,6 +154,7 @@ impl From for u16 { ExtensionType::ExternalPub => 4, ExtensionType::ExternalSenders => 5, ExtensionType::LastResort => 10, + ExtensionType::ProtectedMetadata => 11, ExtensionType::Unknown(unknown) => unknown, } } @@ -162,6 +171,7 @@ impl ExtensionType { | ExtensionType::ExternalPub | ExtensionType::ExternalSenders | ExtensionType::LastResort + | ExtensionType::ProtectedMetadata ) } } @@ -200,6 +210,9 @@ pub enum Extension { /// A [`LastResortExtension`] LastResort(LastResortExtension), + /// A [`ProtectedMetadata`] extension + ProtectedMetadata(ProtectedMetadata), + /// A currently unknown extension. Unknown(u16, UnknownExtension), } @@ -378,6 +391,15 @@ impl Extensions { _ => None, }) } + + /// Get a reference to the [`ProtectedMetadata`] if there is any. + pub fn protected_metadata(&self) -> Option<&ProtectedMetadata> { + self.find_by_type(ExtensionType::ExternalSenders) + .and_then(|e| match e { + Extension::ProtectedMetadata(e) => Some(e), + _ => None, + }) + } } impl Extension { @@ -445,6 +467,18 @@ impl Extension { } } + /// Get a reference to this extension as [`ProtectedMetadata`]. + /// Returns an [`ExtensionError::InvalidExtensionType`] error if called on + /// an [`Extension`] that's not a [`ProtectedMetadata`] extension. + pub fn as_protected_metadata_extension(&self) -> Result<&ProtectedMetadata, ExtensionError> { + match self { + Self::ProtectedMetadata(e) => Ok(e), + _ => Err(ExtensionError::InvalidExtensionType( + "This is not an ProtectedMetadata".into(), + )), + } + } + /// Returns the [`ExtensionType`] #[inline] pub const fn extension_type(&self) -> ExtensionType { @@ -455,6 +489,7 @@ impl Extension { Extension::ExternalPub(_) => ExtensionType::ExternalPub, Extension::ExternalSenders(_) => ExtensionType::ExternalSenders, Extension::LastResort(_) => ExtensionType::LastResort, + Extension::ProtectedMetadata(_) => ExtensionType::ProtectedMetadata, Extension::Unknown(kind, _) => ExtensionType::Unknown(*kind), } } diff --git a/openmls/src/extensions/protected_metadata.rs b/openmls/src/extensions/protected_metadata.rs new file mode 100644 index 0000000000..c87b79bad3 --- /dev/null +++ b/openmls/src/extensions/protected_metadata.rs @@ -0,0 +1,241 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use openmls_traits::signatures::Signer; +use tls_codec::{Serialize as TlsSerializeTrait, TlsDeserialize, TlsSerialize, TlsSize}; + +use super::{Deserialize, Serialize}; +use crate::{ + ciphersuite::{ + signable::{Signable, SignatureError, SignedStruct, Verifiable, VerifiedStruct}, + signature::Signature, + }, + credentials::Credential, +}; + +/// # Protected Metadata +/// +/// ```c +/// struct { +/// opaque signer_application_id; +/// Credential signer_credential; +/// SignaturePublicKey signature_key; +/// uint64 signing_time; +/// opaque metadata; +/// /* SignWithLabel(., "ProtectedMetadataTBS",ProtectedMetadata) */ +/// opaque signature; +/// } ProtectedMetadata; +/// ``` +/// +/// This extension must be verified by the application every time it is set or +/// changed. +/// The application **MUST** verify that +/// * the signature is valid +/// * the credential has been valid at `signing_time` +/// * the `signer_application_id` is equal to the `creator_application_id`. +/// +/// FIXME: This should NOT be deserializable. But we need to change more code for +/// that to be possible. +#[derive( + PartialEq, Eq, Clone, Debug, Serialize, Deserialize, TlsDeserialize, TlsSerialize, TlsSize, +)] +pub struct ProtectedMetadata { + payload: ProtectedMetadataTbs, + signature: Signature, +} + +impl ProtectedMetadata { + /// Create a new protected metadata extension and sign it. + pub fn new( + signer: &impl Signer, + signer_application_id: Vec, + signer_credential: Credential, + signature_key: Vec, + metadata: Vec, + ) -> Result { + let tbs = ProtectedMetadataTbs::new( + signer_application_id, + signer_credential, + signature_key, + metadata, + ); + tbs.sign(signer) + } + + /// Get the signer application ID as slice. + pub fn signer_application_id(&self) -> &[u8] { + self.payload.signer_application_id.as_ref() + } + + /// Get the signer [`Credential`]. + pub fn signer_credential(&self) -> &Credential { + &self.payload.signer_credential + } + + /// Get the signature key as slize. + pub fn signature_key(&self) -> &[u8] { + self.payload.signature_key.as_ref() + } + + /// Get the signing time as UNIX timestamp. + pub fn signing_time(&self) -> u64 { + self.payload.signing_time + } + + /// Get the serialized metadata as slice. + /// + /// This is opaque to OpenMLS. The caller must handle it appropriately. + pub fn metadata(&self) -> &[u8] { + self.payload.metadata.as_ref() + } +} + +impl SignedStruct for ProtectedMetadata { + fn from_payload(payload: ProtectedMetadataTbs, signature: Signature) -> Self { + ProtectedMetadata { payload, signature } + } +} + +/// # Protected Metadata +/// +/// ```c +/// /* SignWithLabel(., "ProtectedMetadataTBS",ProtectedMetadata) */ +/// struct { +/// opaque signer_application_id; +/// Credential signer_credential; +/// SignaturePublicKey signature_key; +/// uint64 signing_time; +/// opaque metadata; +/// } ProtectedMetadataTBS; +/// ``` +#[derive( + PartialEq, Eq, Clone, Debug, Serialize, Deserialize, TlsSerialize, TlsDeserialize, TlsSize, +)] +pub struct ProtectedMetadataTbs { + signer_application_id: Vec, + signer_credential: Credential, + signature_key: Vec, + signing_time: u64, + metadata: Vec, +} + +impl ProtectedMetadataTbs { + /// Create a protected metadata extension tbs. + fn new( + signer_application_id: Vec, + signer_credential: Credential, + signature_key: Vec, + metadata: impl Into>, + ) -> Self { + Self { + signer_application_id, + signer_credential, + signature_key, + signing_time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!") + .as_secs(), + metadata: metadata.into(), + } + } +} + +const SIGNATURE_LABEL: &str = "ProtectedMetadataTbs"; + +impl Signable for ProtectedMetadataTbs { + type SignedOutput = ProtectedMetadata; + + fn unsigned_payload(&self) -> Result, tls_codec::Error> { + self.tls_serialize_detached() + } + + fn label(&self) -> &str { + SIGNATURE_LABEL + } +} + +/// XXX: This really should not be implemented on [`ProtectedMetadata`] but on +/// the verifiable version. +mod verifiable { + use super::*; + + impl Verifiable for ProtectedMetadata { + fn unsigned_payload(&self) -> Result, tls_codec::Error> { + self.payload.tls_serialize_detached() + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn label(&self) -> &str { + SIGNATURE_LABEL + } + } + + mod private_mod { + #[derive(Default)] + pub struct Seal; + } + + impl VerifiedStruct for ProtectedMetadata { + type SealingType = private_mod::Seal; + + fn from_verifiable(v: ProtectedMetadata, _seal: Self::SealingType) -> Self { + Self { + payload: v.payload, + signature: v.signature, + } + } + } +} + +#[cfg(test)] +mod tests { + use tls_codec::{Deserialize, Serialize}; + + use crate::{ + credentials::test_utils::new_credential, extensions::protected_metadata::ProtectedMetadata, + prelude_test::OpenMlsSignaturePublicKey, test_utils::*, + }; + + use super::*; + + #[apply(ciphersuites_and_providers)] + fn serialize_extension(ciphersuite: Ciphersuite, provider: &impl OpenMlsProvider) { + let creator_application_id = b"MetadataTestAppId".to_vec(); + + // Create metadata + let metadata = vec![1, 2, 3]; + + // Setup crypto + let (credential_with_key, signer) = new_credential( + provider, + b"Kreator", + crate::credentials::CredentialType::Basic, + ciphersuite.signature_algorithm(), + ); + let signature_key = + OpenMlsSignaturePublicKey::new(signer.public().into(), ciphersuite.into()).unwrap(); + + let signer_application_id = creator_application_id.clone(); + let extension = ProtectedMetadata::new( + &signer, + signer_application_id, + credential_with_key.credential.clone(), + signature_key.as_slice().to_vec(), + metadata.clone(), + ) + .unwrap(); + + // serialize and deserialize + verify + let serialized = extension.tls_serialize_detached().unwrap(); + let unverified = ProtectedMetadata::tls_deserialize_exact(serialized).unwrap(); + let deserialized: ProtectedMetadata = unverified + .verify(provider.crypto(), &signature_key) + .unwrap(); + assert_eq!(deserialized, extension); + + let xmtp_metadata = deserialized.metadata(); + assert_eq!(xmtp_metadata, metadata); + } +}