diff --git a/Cargo.lock b/Cargo.lock index 183c5dafd..d514eeb45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1623,6 +1623,15 @@ name = "hex" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "hkdf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hmac" version = "0.7.1" @@ -2253,6 +2262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "mls" version = "0.5.0" dependencies = [ + "hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "ra-client 0.5.0", "ra-enclave 0.5.0", "ring 0.16.13 (git+https://github.com/crypto-com/ring.git?rev=4e1862fb0df9efaf2d2c1ec8cacb1e53104f3daa)", @@ -5090,6 +5100,7 @@ dependencies = [ "checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" "checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" "checksum hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +"checksum hkdf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" "checksum hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" "checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" diff --git a/chain-tx-enclave-next/mls/Cargo.toml b/chain-tx-enclave-next/mls/Cargo.toml index b3501c44f..7c4014d23 100644 --- a/chain-tx-enclave-next/mls/Cargo.toml +++ b/chain-tx-enclave-next/mls/Cargo.toml @@ -12,7 +12,7 @@ thiserror = "1.0" rustls = "0.17" x509-parser = "0.7" sha2 = "0.8.1" - +hkdf = "0.8" ra-client = { path = "../enclave-ra/ra-client" } [target.'cfg(target_env = "sgx")'.dependencies] diff --git a/chain-tx-enclave-next/mls/src/group.rs b/chain-tx-enclave-next/mls/src/group.rs index 2fb3763e9..9b33db5f0 100644 --- a/chain-tx-enclave-next/mls/src/group.rs +++ b/chain-tx-enclave-next/mls/src/group.rs @@ -3,7 +3,9 @@ use crate::keypackage::Timespec; use crate::keypackage::{self as kp, KeyPackage, OwnedKeyPackage}; use crate::message::*; use crate::tree::*; +use crate::utils::{encode_vec_u8_u8, read_vec_u8_u8}; use ra_client::EnclaveCertVerifier; +use rustls::internal::msgs::codec::{self, Codec, Reader}; use sha2::{Digest, Sha256}; use std::collections::BTreeSet; @@ -40,16 +42,51 @@ impl GroupAux { } } - fn add_init(&mut self, _kp: &KeyPackage) -> MLSPlaintext { - todo!() + fn get_sender(&self) -> Sender { + Sender { + sender_type: SenderType::Member, + sender: self.tree.my_pos as u32, + } + } + + fn get_signed_add(&self, kp: &KeyPackage) -> MLSPlaintext { + let sender = self.get_sender(); + let add_content = MLSPlaintextCommon { + group_id: self.context.group_id.clone(), + epoch: self.context.epoch, + sender, + authenticated_data: vec![], + content: ContentType::Proposal(Proposal::Add(Add { + key_package: kp.clone(), + })), + }; + let to_be_signed = MLSPlaintextTBS { + context: self.context.clone(), + content: add_content.clone(), + } + .get_encoding(); + let signature = self.owned_kp.private_key.sign(&to_be_signed); + MLSPlaintext { + content: add_content, + signature, + } } fn get_confirmed_transcript_hash(&self, _commit: &Commit) -> Vec { todo!() } - fn get_signed_commit(&self, _plain: &MLSPlaintextCommon) -> MLSPlaintext { - todo!() + fn get_signed_commit(&self, plain: &MLSPlaintextCommon) -> MLSPlaintext { + let to_be_signed = MLSPlaintextTBS { + context: self.context.clone(), // TODO: current or next context? + content: plain.clone(), + } + .get_encoding(); + let signature = self.owned_kp.private_key.sign(&to_be_signed); + MLSPlaintext { + content: plain.clone(), + signature, + } } fn get_welcome_msg(&self) -> Welcome { @@ -78,14 +115,12 @@ impl GroupAux { .generate_new_epoch_secrets(commit_secret, &updated_group_context); let confirmation = epoch_secrets.compute_confirmation(&updated_group_context.confirmed_transcript_hash); - let sender = Sender { - sender_type: SenderType::Member, - sender: 0, // FIXME - }; + let sender = self.get_sender(); let commit_content = MLSPlaintextCommon { group_id: self.context.group_id.clone(), epoch: self.context.epoch, sender, + authenticated_data: vec![], content: ContentType::Commit { commit, confirmation, @@ -119,7 +154,7 @@ impl GroupAux { let (context, tree) = GroupContext::init(creator_kp.keypackage.clone())?; let mut group = GroupAux::new(context, tree, creator_kp); let add_proposals: Vec = - others.iter().map(|kp| group.add_init(kp)).collect(); + others.iter().map(|kp| group.get_signed_add(kp)).collect(); let (commit, welcome) = group.init_commit(&add_proposals); Ok((group, add_proposals, commit, welcome)) } @@ -151,7 +186,7 @@ impl CipherSuite { const TDBE_GROUP_ID: &[u8] = b"Crypto.com Chain Council Node Transaction Data Bootstrap Enclave"; /// spec: draft-ietf-mls-protocol.md#group-state -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct GroupContext { /// 0..255 bytes -- application-defined id pub group_id: Vec, @@ -168,9 +203,35 @@ pub struct GroupContext { /// the messages that led to this state. /// 0..255 pub confirmed_transcript_hash: Vec, + /// 0..2^16-1 pub extensions: Vec, } +impl Codec for GroupContext { + fn encode(&self, bytes: &mut Vec) { + encode_vec_u8_u8(bytes, &self.group_id); + self.epoch.encode(bytes); + encode_vec_u8_u8(bytes, &self.tree_hash); + encode_vec_u8_u8(bytes, &self.confirmed_transcript_hash); + codec::encode_vec_u16(bytes, &self.extensions); + } + + fn read(r: &mut Reader) -> Option { + let group_id = read_vec_u8_u8(r)?; + let epoch = u64::read(r)?; + let tree_hash = read_vec_u8_u8(r)?; + let confirmed_transcript_hash = read_vec_u8_u8(r)?; + let extensions = codec::read_vec_u16(r)?; + Some(Self { + group_id, + epoch, + tree_hash, + confirmed_transcript_hash, + extensions, + }) + } +} + impl GroupContext { pub fn init(creator_kp: KeyPackage) -> Result<(Self, Tree), kp::Error> { let extensions = creator_kp.payload.extensions.clone(); @@ -187,3 +248,62 @@ impl GroupContext { )) } } + +#[cfg(test)] +mod test { + + use super::*; + use crate::credential::Credential; + use crate::extensions::{self as ext, MLSExtension}; + use crate::key::PrivateKey; + use crate::keypackage::{ + KeyPackage, KeyPackagePayload, OwnedKeyPackage, MLS10_128_DHKEMP256_AES128GCM_SHA256_P256, + PROTOCOL_VERSION_MLS10, + }; + use rustls::internal::msgs::codec::Codec; + + fn get_fake_keypackage() -> OwnedKeyPackage { + let keypair = ring::signature::EcdsaKeyPair::generate_pkcs8( + &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &ring::rand::SystemRandom::new(), + ) + .unwrap(); + let extensions = vec![ + ext::SupportedVersionsExt(vec![PROTOCOL_VERSION_MLS10]).entry(), + ext::SupportedCipherSuitesExt(vec![MLS10_128_DHKEMP256_AES128GCM_SHA256_P256]).entry(), + ext::LifeTimeExt::new(0, 100).entry(), + ]; + + let private_key = PrivateKey::from_pkcs8(keypair.as_ref()).expect("invalid private key"); + let payload = KeyPackagePayload { + version: PROTOCOL_VERSION_MLS10, + cipher_suite: MLS10_128_DHKEMP256_AES128GCM_SHA256_P256, + init_key: private_key.public_key(), + credential: Credential::X509(vec![]), + extensions, + }; + + // sign payload + let signature = private_key.sign(&payload.get_encoding()); + + OwnedKeyPackage { + keypackage: KeyPackage { payload, signature }, + private_key, + } + } + + #[test] + fn test_sign_verify_add() { + let creator_kp = get_fake_keypackage(); + let to_be_added = get_fake_keypackage().keypackage; + let (context, tree) = GroupContext::init(creator_kp.keypackage.clone()).unwrap(); + let group_aux = GroupAux::new(context, tree, creator_kp); + let plain = group_aux.get_signed_add(&to_be_added); + assert!(plain + .verify_signature( + &group_aux.context, + &group_aux.owned_kp.private_key.public_key() + ) + .is_ok()); + } +} diff --git a/chain-tx-enclave-next/mls/src/key.rs b/chain-tx-enclave-next/mls/src/key.rs index 3d46185f2..4c53d4d31 100644 --- a/chain-tx-enclave-next/mls/src/key.rs +++ b/chain-tx-enclave-next/mls/src/key.rs @@ -14,6 +14,7 @@ pub struct PublicKey(Vec); impl PublicKey { /// Verify P-256 signature + /// FIXME: types to distinguish between signature and message payloads pub fn verify_signature(&self, msg: &[u8], sig: &[u8]) -> Result<(), error::Unspecified> { ECDSA_P256_SHA256_ASN1.verify(self.0.as_slice().into(), msg.into(), sig.into()) } diff --git a/chain-tx-enclave-next/mls/src/message.rs b/chain-tx-enclave-next/mls/src/message.rs index b67a9e8a6..8fb796edb 100644 --- a/chain-tx-enclave-next/mls/src/message.rs +++ b/chain-tx-enclave-next/mls/src/message.rs @@ -1,27 +1,33 @@ +use crate::group::GroupContext; use crate::key::PublicKey; use crate::keypackage::{CipherSuite, KeyPackage, ProtocolVersion}; +use crate::utils::{ + decode_option, encode_option, encode_vec_u32, encode_vec_u8_u16, encode_vec_u8_u8, + read_vec_u32, read_vec_u8_u16, read_vec_u8_u8, +}; +use rustls::internal::msgs::codec::{self, Codec, Reader}; /// spec: draft-ietf-mls-protocol.md#Add +#[derive(Debug, Clone)] pub struct Add { - key_package: KeyPackage, + pub key_package: KeyPackage, } /// spec: draft-ietf-mls-protocol.md#Update -/// FIXME -#[allow(dead_code)] +#[derive(Debug, Clone)] pub struct Update { - key_package: KeyPackage, + pub key_package: KeyPackage, } /// spec: draft-ietf-mls-protocol.md#Remove -/// FIXME -#[allow(dead_code)] +#[derive(Debug, Clone)] pub struct Remove { - removed: u32, + pub removed: u32, } /// spec: draft-ietf-mls-protocol.md#Proposal /// #[repr(u8)] +#[derive(Debug, Clone)] pub enum Proposal { // Invalid = 0, Add(Add), // = 1, @@ -29,8 +35,47 @@ pub enum Proposal { Remove(Remove), // = 3, } +impl Codec for Proposal { + fn encode(&self, bytes: &mut Vec) { + match self { + Proposal::Add(Add { key_package }) => { + 1u8.encode(bytes); + key_package.encode(bytes); + } + Proposal::Update(Update { key_package }) => { + 2u8.encode(bytes); + key_package.encode(bytes); + } + Proposal::Remove(Remove { removed }) => { + 3u8.encode(bytes); + removed.encode(bytes); + } + } + } + + fn read(r: &mut Reader) -> Option { + let tag = u8::read(r)?; + match tag { + 1 => { + let key_package = KeyPackage::read(r)?; + Some(Proposal::Add(Add { key_package })) + } + 2 => { + let key_package = KeyPackage::read(r)?; + Some(Proposal::Update(Update { key_package })) + } + 3 => { + let removed = u32::read(r)?; + Some(Proposal::Remove(Remove { removed })) + } + _ => None, + } + } +} + /// spec: draft-ietf-mls-protocol.md#Message-Framing /// + draft-ietf-mls-protocol.md#MContent-Signing-and-Encryption +#[derive(Debug, Clone)] pub struct MLSPlaintextCommon { /// 0..255 bytes -- application-defined id pub group_id: Vec, @@ -39,16 +84,130 @@ pub struct MLSPlaintextCommon { /// that is processed) pub epoch: u64, pub sender: Sender, + /// 0..2^32-1 + pub authenticated_data: Vec, pub content: ContentType, } -/// spec: draft-ietf-mls-protocol.md#Message-Framing +impl Codec for MLSPlaintextCommon { + fn encode(&self, bytes: &mut Vec) { + encode_vec_u8_u8(bytes, &self.group_id); + self.epoch.encode(bytes); + self.sender.encode(bytes); + encode_vec_u32(bytes, &self.authenticated_data); + match &self.content { + ContentType::Application { application_data } => { + 1u8.encode(bytes); + encode_vec_u32(bytes, &application_data); + } + ContentType::Proposal(p) => { + 2u8.encode(bytes); + p.encode(bytes); + } + ContentType::Commit { + commit, + confirmation, + } => { + 3u8.encode(bytes); + commit.encode(bytes); + encode_vec_u8_u8(bytes, confirmation); + } + } + } + + fn read(r: &mut Reader) -> Option { + let group_id = read_vec_u8_u8(r)?; + let epoch = u64::read(r)?; + let sender = Sender::read(r)?; + let authenticated_data: Vec = read_vec_u32(r)?; + let tag = u8::read(r)?; + let content = match tag { + 1 => { + let application_data: Vec = read_vec_u32(r)?; + Some(ContentType::Application { application_data }) + } + 2 => { + let proposal = Proposal::read(r)?; + Some(ContentType::Proposal(proposal)) + } + 3 => { + let commit = Commit::read(r)?; + let confirmation = read_vec_u8_u8(r)?; + Some(ContentType::Commit { + commit, + confirmation, + }) + } + _ => None, + }?; + Some(MLSPlaintextCommon { + group_id, + epoch, + sender, + authenticated_data, + content, + }) + } +} + +/// spec: draft-ietf-mls-protocol.md#Message-Framing\ +#[derive(Debug)] pub struct MLSPlaintext { pub content: MLSPlaintextCommon, /// 0..2^16-1 pub signature: Vec, } +impl Codec for MLSPlaintext { + fn encode(&self, bytes: &mut Vec) { + self.content.encode(bytes); + encode_vec_u8_u16(bytes, &self.signature); + } + + fn read(r: &mut Reader) -> Option { + let content = MLSPlaintextCommon::read(r)?; + let signature = read_vec_u8_u16(r)?; + Some(MLSPlaintext { content, signature }) + } +} + +impl MLSPlaintext { + pub fn verify_signature( + &self, + context: &GroupContext, + public_key: &PublicKey, + ) -> Result<(), ring::error::Unspecified> { + let payload = MLSPlaintextTBS { + context: context.clone(), + content: self.content.clone(), + } + .get_encoding(); + public_key.verify_signature(&payload, &self.signature) + } +} + +/// payload to be signed +/// spec: draft-ietf-mls-protocol.md#Content-Signing-and-Encryption +#[derive(Debug)] +pub struct MLSPlaintextTBS { + /// TODO: https://github.com/mlswg/mls-protocol/issues/323 may be removed? + pub context: GroupContext, + pub content: MLSPlaintextCommon, +} + +impl Codec for MLSPlaintextTBS { + fn encode(&self, bytes: &mut Vec) { + self.context.encode(bytes); + self.content.encode(bytes); + } + + fn read(r: &mut Reader) -> Option { + let context = GroupContext::read(r)?; + let content = MLSPlaintextCommon::read(r)?; + Some(MLSPlaintextTBS { context, content }) + } +} + impl MLSPlaintext { pub fn get_add_keypackage(&self) -> Option { match &self.content.content { @@ -60,9 +219,22 @@ impl MLSPlaintext { /// 0..255 -- hash of the MLSPlaintext in which the Proposal was sent /// spec: draft-ietf-mls-protocol.md#Commit -pub type ProposalId = Vec; +#[derive(Debug, Clone)] +pub struct ProposalId(pub Vec); + +impl Codec for ProposalId { + fn encode(&self, bytes: &mut Vec) { + encode_vec_u8_u8(bytes, &self.0); + } + + fn read(r: &mut Reader) -> Option { + let pid = read_vec_u8_u8(r)?; + Some(ProposalId(pid)) + } +} /// spec: draft-ietf-mls-protocol.md#Commit +#[derive(Debug, Clone)] pub struct Commit { /// 0..2^16-1 pub updates: Vec, @@ -70,12 +242,34 @@ pub struct Commit { pub removes: Vec, /// 0..2^16-1 pub adds: Vec, - /// 0..2^16-1 /// "path field of a Commit message MUST be populated if the Commit covers at least one Update or Remove proposal" /// "path field MUST also be populated if the Commit covers no proposals at all (i.e., if all three proposal vectors are empty)." pub path: Option, } +impl Codec for Commit { + fn encode(&self, bytes: &mut Vec) { + codec::encode_vec_u16(bytes, &self.updates); + codec::encode_vec_u16(bytes, &self.removes); + codec::encode_vec_u16(bytes, &self.adds); + encode_option(bytes, &self.path); + } + + fn read(r: &mut Reader) -> Option { + let updates: Vec = codec::read_vec_u16(r)?; + let removes: Vec = codec::read_vec_u16(r)?; + let adds: Vec = codec::read_vec_u16(r)?; + let path: Option = decode_option(r)?; + + Some(Commit { + updates, + removes, + adds, + path, + }) + } +} + /// spec: draft-ietf-mls-protocol.md#Welcoming-New-Members pub struct Welcome { pub version: ProtocolVersion, @@ -93,6 +287,7 @@ pub struct EncryptedGroupSecrets { } /// spec: draft-ietf-mls-protocol.md#Direct-Paths +#[derive(Debug, Clone)] pub struct HPKECiphertext { /// 0..2^16-1 pub kem_output: Vec, @@ -100,23 +295,77 @@ pub struct HPKECiphertext { pub ciphertext: Vec, } +impl Codec for HPKECiphertext { + fn encode(&self, bytes: &mut Vec) { + encode_vec_u8_u16(bytes, &self.kem_output); + encode_vec_u8_u16(bytes, &self.ciphertext); + } + + fn read(r: &mut Reader) -> Option { + let kem_output = read_vec_u8_u16(r)?; + let ciphertext = read_vec_u8_u16(r)?; + + Some(HPKECiphertext { + kem_output, + ciphertext, + }) + } +} + /// spec: draft-ietf-mls-protocol.md#Direct-Paths +#[derive(Debug, Clone)] pub struct DirectPathNode { pub public_key: PublicKey, - /// 0..0..2^32-1> + /// 0..2^32-1 pub encrypted_path_secret: Vec, } +impl Codec for DirectPathNode { + fn encode(&self, bytes: &mut Vec) { + self.public_key.encode(bytes); + encode_vec_u32(bytes, &self.encrypted_path_secret); + } + + fn read(r: &mut Reader) -> Option { + let public_key = PublicKey::read(r)?; + let encrypted_path_secret: Vec = read_vec_u32(r)?; + + Some(DirectPathNode { + public_key, + encrypted_path_secret, + }) + } +} + /// spec: draft-ietf-mls-protocol.md#Direct-Paths +#[derive(Debug, Clone)] pub struct DirectPath { pub leaf_key_package: KeyPackage, - /// 0..0..2^16-1> + /// 0..2^16-1 pub nodes: Vec, } +impl Codec for DirectPath { + fn encode(&self, bytes: &mut Vec) { + self.leaf_key_package.encode(bytes); + codec::encode_vec_u16(bytes, &self.nodes); + } + + fn read(r: &mut Reader) -> Option { + let leaf_key_package = KeyPackage::read(r)?; + let nodes: Vec = codec::read_vec_u16(r)?; + + Some(DirectPath { + leaf_key_package, + nodes, + }) + } +} + /// spec: draft-ietf-mls-protocol.md#Message-Framing /// #[repr(u8)] #[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] pub enum ContentType { Application { // <0..2^32-1> @@ -132,6 +381,7 @@ pub enum ContentType { /// spec: draft-ietf-mls-protocol.md#Message-Framing #[repr(u8)] +#[derive(Debug, Copy, Clone)] pub enum SenderType { Member = 1, Preconfigured = 2, @@ -139,7 +389,30 @@ pub enum SenderType { } /// spec: draft-ietf-mls-protocol.md#Message-Framing +#[derive(Debug, Clone)] pub struct Sender { pub sender_type: SenderType, pub sender: u32, } + +impl Codec for Sender { + fn encode(&self, bytes: &mut Vec) { + (self.sender_type as u8).encode(bytes); + self.sender.encode(bytes); + } + + fn read(r: &mut Reader) -> Option { + let sender_t = u8::read(r)?; + let sender_type = match sender_t { + 1 => Some(SenderType::Member), + 2 => Some(SenderType::Preconfigured), + 3 => Some(SenderType::NewMember), + _ => None, + }?; + let sender = u32::read(r)?; + Some(Self { + sender_type, + sender, + }) + } +} diff --git a/chain-tx-enclave-next/mls/src/tree.rs b/chain-tx-enclave-next/mls/src/tree.rs index 4f10d69ec..9a1b2d3f0 100644 --- a/chain-tx-enclave-next/mls/src/tree.rs +++ b/chain-tx-enclave-next/mls/src/tree.rs @@ -111,8 +111,14 @@ impl Codec for LeafNodeHashInput { /// TODO: https://github.com/mlswg/mls-protocol/pull/327/files #[derive(Clone)] pub struct Tree { + /// all tree nodes stored in a vector pub nodes: Vec, + /// the used ciphersuite (for hashing etc.) + /// TODO: unify with keypackage one pub cs: CipherSuite, + /// position of the participant in the tree + /// TODO: leaf vs node position? + pub my_pos: usize, } /// The level of a node in the tree. Leaves are level 0, their @@ -326,10 +332,12 @@ impl Tree { Ok(Self { nodes: vec![Node::Leaf(Some(creator_kp))], cs, + my_pos: 0, }) } } +#[cfg(test)] mod test { #[test]