diff --git a/fuzz/fuzz_targets/payload_encode_decode.rs b/fuzz/fuzz_targets/payload_encode_decode.rs index b6a09f7f..def31d48 100644 --- a/fuzz/fuzz_targets/payload_encode_decode.rs +++ b/fuzz/fuzz_targets/payload_encode_decode.rs @@ -15,6 +15,7 @@ fuzz_target!(|data: cesr::fuzzing::Wrapper| { cesr::Payload::RoutedMessage(route, _) => assert!(route.is_empty()), _ => todo!(), }, + Err(cesr::error::EncodeError::InvalidVid) => {} _ => todo!(), } }); diff --git a/tsp_sdk/src/cesr/consts.rs b/tsp_sdk/src/cesr/consts.rs new file mode 100644 index 00000000..d14114a7 --- /dev/null +++ b/tsp_sdk/src/cesr/consts.rs @@ -0,0 +1,46 @@ +macro_rules! cesr { + ($value: expr) => { + $crate::cesr::consts::cesr_int($value) as _ + }; +} + +pub(super) use cesr; + +/// A function for more easy encoding of CESR constants +pub const fn cesr_int(x: &str) -> u64 { + let x = x.as_bytes(); + let mut acc = 0; + let mut i = 0; + while i < x.len() { + let ch = x[i]; + acc = acc << 6 + | match ch { + ch if ch.is_ascii_uppercase() => ch - b'A', + ch if ch.is_ascii_lowercase() => ch - b'a' + 26, + ch if ch.is_ascii_digit() => ch - b'0' + 52, + b'-' => 62, + b'_' => 63, + _ => panic!("not a base64url character"), + } as u64; + i += 1; + } + + acc +} + +/// A function for giving the contents of "TSP_TYPECODE" fields +pub const fn cesr_data(x: &str) -> [u8; N] { + let val = cesr_int(x); + assert!(val < (1u64 << (8 * N))); + // we canot use 'try_into().unwrap()' here + let src = u64::to_be_bytes(val); + let start = src.len() - N; + let mut result = [0; N]; + let mut i = start; + while i < src.len() { + result[i - start] = src[i]; + i += 1 + } + + result +} diff --git a/tsp_sdk/src/cesr/decode.rs b/tsp_sdk/src/cesr/decode.rs index 1ad37a74..3b9317b7 100644 --- a/tsp_sdk/src/cesr/decode.rs +++ b/tsp_sdk/src/cesr/decode.rs @@ -94,6 +94,7 @@ pub fn decode_variable_data_index( } } +#[allow(dead_code)] pub fn decode_variable_data<'a>(identifier: u32, stream: &mut &'a [u8]) -> Option<&'a [u8]> { let range = decode_variable_data_index(identifier, stream, &mut 0)?; let slice = &stream[range.start..range.end]; @@ -113,6 +114,20 @@ pub fn decode_variable_data_mut( Some((slice, stream)) } +pub fn opt_decode_variable_data_mut( + identifier: u32, + stream: &mut [u8], +) -> (Option<&[u8]>, &mut [u8]) { + let Some(range) = decode_variable_data_index(identifier, stream, &mut 0) else { + return (None, stream); + }; + + let (prefix, stream) = stream.split_at_mut(range.end); + let slice = &mut prefix[range.start..]; + + (Some(slice), stream) +} + /// Decode indexed data with a known identifier #[allow(dead_code)] pub fn decode_indexed_data<'a, const N: usize>( @@ -153,22 +168,29 @@ pub fn decode_indexed_data<'a, const N: usize>( } /// Decode a frame with known identifier and size -pub fn decode_count(identifier: u16, stream: &mut &[u8]) -> Option { +pub fn decode_count(identifier: u16, stream: &mut &[u8]) -> Option { let word = extract_triplet(stream.get(0..=2)?.try_into().unwrap()); let index = word & mask(12); let expected = (DASH << 18) | (bits(identifier, 6) << 12) | bits(index, 12); + let expected_long = + (DASH << 18) | D0 << 12 | (bits(identifier, 6) << 6) | bits(index & 0x3F, 6); if word == expected { *stream = &stream[3..]; - Some(index as u16) + Some((index as u16).into()) + } else if word == expected_long { + let next = extract_triplet(stream.get(3..=5)?.try_into().unwrap()); + *stream = &stream[6..]; + + Some(index << 24 | next) } else { None } } /// Decode a frame with known identifier and size -pub fn decode_count_mut(identifier: u16, stream: &mut [u8]) -> Option<(u16, &mut [u8])> { +pub fn decode_count_mut(identifier: u16, stream: &mut [u8]) -> Option<(u32, &mut [u8])> { let mut const_stream: &[u8] = stream; let count = decode_count(identifier, &mut const_stream)?; let offset = stream.len() - const_stream.len(); @@ -193,37 +215,3 @@ pub fn decode_genus( Some(()) } - -/// Decode variable size data (intended size >=50mb) that doesn't fit in known CESR encodings. -/// -/// Since CESR has no native type for this, we use the convention that "big data" -/// Is announced by encoding the size as a 64bit integer (prefix "N"), which is then -/// followed by the raw data padded in the usual sense for CESR (pre-padding) -/// This is a temporary encoding, depending on how CESR will address this in the future. -pub fn decode_large_blob_index(stream: &[u8]) -> Option> { - let selector = b'N' - b'A'; - let size = u64::from_be_bytes(*decode_fixed_data( - selector as u32, - &mut <_>::clone(&stream), - )?) as usize; - let padded_size = size.next_multiple_of(3); - let lead_bytes = padded_size - size; - - let start = 9usize.checked_add(lead_bytes)?; - let end = 9usize.checked_add(padded_size)?; - - let range = start..end; - // make sure the range is valid before returning it - stream.get(range.clone())?; - - Some(range) -} - -#[cfg(test)] -pub fn decode_large_blob<'a>(stream: &mut &'a [u8]) -> Option<&'a [u8]> { - let range = decode_large_blob_index(stream)?; - let slice = &stream[range.start..range.end]; - *stream = &stream[range.end..]; - - Some(slice) -} diff --git a/tsp_sdk/src/cesr/encode.rs b/tsp_sdk/src/cesr/encode.rs index 83d9d988..0bfe051d 100644 --- a/tsp_sdk/src/cesr/encode.rs +++ b/tsp_sdk/src/cesr/encode.rs @@ -68,10 +68,24 @@ pub fn encode_variable_data( } /// Encode a frame with known identifier and count code -pub fn encode_count(identifier: u16, count: u16, stream: &mut impl for<'a> Extend<&'a u8>) { - let word = (DASH << 18) | (bits(identifier, 6) << 12) | bits(count, 12); +pub fn encode_count( + identifier: u16, + count: impl Into, + stream: &mut impl for<'a> Extend<&'a u8>, +) { + let count: usize = count.into(); + let count: u32 = count.try_into().unwrap(); + if count < 4096 { + let word = (DASH << 18) | (bits(identifier, 6) << 12) | bits(count, 12); + + stream.extend(&u32::to_be_bytes(word)[1..]); + } else { + let word1 = (DASH << 18) | (D0 << 12) | (bits(identifier, 6) << 6) | bits(count >> 24, 6); + let word2 = bits(count, 24); - stream.extend(&u32::to_be_bytes(word)[1..]); + stream.extend(&u32::to_be_bytes(word1)[1..]); + stream.extend(&u32::to_be_bytes(word2)[1..]); + } } /// Encode a genus with known identifier and version @@ -88,20 +102,3 @@ pub fn encode_genus( stream.extend(&u32::to_be_bytes(word1)[1..]); stream.extend(&u32::to_be_bytes(word2)[1..]); } - -/// Encode variable size data (intended size >=50mb) that doesn't fit in known CESR encodings. -/// -/// Since CESR has no native type for this, we use the convention that "big data" -/// Is announced by encoding the size as a 64bit integer (prefix "N"), which is then -/// followed by the raw data padded in the usual sense for CESR (pre-padding) -/// This is a temporary encoding, depending on how CESR will address this in the future. -pub fn encode_large_blob(payload: &[u8], stream: &mut impl for<'a> Extend<&'a u8>) { - let size = payload.len() as u64; - let padded_size = size.next_multiple_of(3); - let lead_bytes = padded_size - size; - - let selector = (b'N' - b'A') as u32; - encode_fixed_data(selector, &u64::to_be_bytes(size), stream); - stream.extend(&<[u8; 2]>::default()[0..lead_bytes as usize]); - stream.extend(payload); -} diff --git a/tsp_sdk/src/cesr/error.rs b/tsp_sdk/src/cesr/error.rs index 95d20684..f1bab56e 100644 --- a/tsp_sdk/src/cesr/error.rs +++ b/tsp_sdk/src/cesr/error.rs @@ -3,6 +3,8 @@ pub enum EncodeError { ExcessiveFieldSize, MissingHops, + MissingReceiver, + InvalidVid, } /// An error type to indicate something went wrong with decoding @@ -16,6 +18,9 @@ pub enum DecodeError { VersionMismatch, InvalidCryptoType, InvalidSignatureType, + MissingHops, + UnknownCrypto, + InvalidCrypto, } impl std::fmt::Display for EncodeError { diff --git a/tsp_sdk/src/cesr/mod.rs b/tsp_sdk/src/cesr/mod.rs index 2821e4af..4b270cfd 100644 --- a/tsp_sdk/src/cesr/mod.rs +++ b/tsp_sdk/src/cesr/mod.rs @@ -6,6 +6,7 @@ pub mod error; mod packet; use base64ct::{Base64UrlUnpadded, Encoding}; use error::DecodeError; +mod consts; pub use packet::*; #[cfg(feature = "cesr-t")] @@ -58,8 +59,7 @@ mod selector { /// (Temporary) interface to get Sender/Receiver VIDs information from a CESR-encoded message pub fn get_sender_receiver(message: &[u8]) -> Result<(&[u8], Option<&[u8]>), error::DecodeError> { - let mut stream = message; - let (sender, receiver, _, _) = decode_sender_receiver(&mut stream)?; + let (sender, receiver, _, _) = decode_sender_receiver(message)?; Ok((sender, receiver)) } @@ -100,8 +100,10 @@ impl EnvelopeType<'_> { } } +//TODO: simplify the source of sender/receiver pub fn probe(stream: &mut [u8]) -> Result, error::DecodeError> { - let (_, crypto_type, _) = detected_tsp_header_size_and_confidentiality(&mut (stream as &[u8]))?; + let (_sender, _receiver, crypto_type, _) = + detected_tsp_header_size_and_confidentiality(stream, &mut 0)?; let envelope = decode_envelope(stream)? .into_opened() @@ -184,7 +186,7 @@ mod test { &mut data, ); // 1 lead byte encode_variable_data(42, b"I always speak the truth. Not the whole truth, because there's no way, to say it all.", &mut data); // 2 lead bytes - encode_count(7, 2, &mut data); + encode_count(7, 2usize, &mut data); encode_indexed_data(5, 57, b"DON'T PANIC!", &mut data); // 0 lead bytes encode_indexed_data(5, 0, b"SECRET KEY", &mut data); // 2 lead bytes @@ -420,24 +422,4 @@ ACTD7NDX93ZGTkZBBuSeSGsAQ7u0hngpNTZTK_Um7rUZGnLRNJvo5oOnnC1J2iBQHuxoq8PyjdT3BHS2 assert_eq!(decode_indexed_data::<64>(0, slice).unwrap().0, 1); assert_eq!(decode_indexed_data::<64>(0, slice).unwrap().0, 2); } - - #[test] - fn blob_encode_decode() { - let mut data = vec![]; - encode_large_blob(b"Where there is power, there is resistance.", &mut data); // 0 lead bytes - encode_large_blob(b"TrustSpanP!", &mut data); // 1 lead byte - encode_large_blob(b"I always speak the truth. Not the whole truth, because there's no way, to say it all.", &mut data); // 2 lead bytes - encode_variable_data(5, b"TrustSpanP!", &mut data); // not a blob - let mut input = &data[..]; - assert_eq!( - decode_large_blob(&mut input).unwrap(), - b"Where there is power, there is resistance." - ); - assert_eq!(decode_large_blob(&mut input).unwrap(), b"TrustSpanP!"); - assert_eq!( - decode_large_blob(&mut input).unwrap(), - b"I always speak the truth. Not the whole truth, because there's no way, to say it all." - ); - assert!(decode_large_blob(&mut input).is_none()); - } } diff --git a/tsp_sdk/src/cesr/packet.rs b/tsp_sdk/src/cesr/packet.rs index eff345e9..8f837b48 100644 --- a/tsp_sdk/src/cesr/packet.rs +++ b/tsp_sdk/src/cesr/packet.rs @@ -1,42 +1,61 @@ +use super::consts::{cesr, cesr_data}; + +/// The TSP version supported by this spec +const TSP_VERSION: (u16, u8, u8) = (0, 0, 1); + /// Constants that determine the specific CESR types for "variable length data" -const TSP_PLAINTEXT: u32 = (b'B' - b'A') as u32; -const TSP_CIPHERTEXT: u32 = (b'C' - b'A') as u32; -const TSP_DEVELOPMENT_VID: u32 = (((21 << 6) | 8) << 6) | 3; // "VID" +const TSP_PLAINTEXT: u32 = cesr!("B"); +const TSP_NACL_CIPHERTEXT: u32 = cesr!("C"); +const TSP_NACLAUTH_CIPHERTEXT: u32 = cesr!("NCL"); +const TSP_HPKEBASE_CIPHERTEXT: u32 = cesr!("F"); +const TSP_HPKEAUTH_CIPHERTEXT: u32 = cesr!("G"); +#[cfg(feature = "pq")] +const TSP_HPKEPQ_CIPHERTEXT: u32 = cesr!("PQC"); +const TSP_VID: u32 = cesr!("B"); /// Constants that determine the specific CESR types for "fixed length data" -const TSP_TYPECODE: u32 = (b'X' - b'A') as u32; -const ED25519_SIGNATURE: u32 = (b'B' - b'A') as u32; +const ED25519_SIGNATURE: u32 = cesr!("B"); #[cfg(feature = "pq")] -const ML_DSA_65_SIGNATURE: u32 = 0o170413u32; // QDM +const ML_DSA_65_SIGNATURE: u32 = cesr!("QDM"); #[allow(clippy::eq_op)] -const TSP_NONCE: u32 = (b'A' - b'A') as u32; -const TSP_SHA256: u32 = (b'I' - b'A') as u32; +const TSP_NONCE: u32 = cesr!("A"); +const TSP_SHA256: u32 = cesr!("I"); #[allow(dead_code)] -const TSP_BLAKE2B256: u32 = (b'F' - b'A') as u32; +const TSP_BLAKE2B256: u32 = cesr!("F"); /// Constants that determine the specific CESR types for the framing codes -const TSP_ETS_WRAPPER: u16 = (b'E' - b'A') as u16; -const TSP_S_WRAPPER: u16 = (b'S' - b'A') as u16; -const TSP_HOP_LIST: u16 = (b'I' - b'A') as u16; -const TSP_PAYLOAD: u16 = (b'Z' - b'A') as u16; - -/// Constants to encode message types -mod msgtype { - pub(super) const GEN_MSG: [u8; 2] = [0, 0]; - pub(super) const NEST_MSG: [u8; 2] = [0, 1]; - pub(super) const NEW_REL: [u8; 2] = [1, 0]; - pub(super) const NEW_REL_REPLY: [u8; 2] = [1, 1]; - pub(super) const NEW_NEST_REL: [u8; 2] = [1, 2]; - pub(super) const NEW_NEST_REL_REPLY: [u8; 2] = [1, 3]; - pub(super) const NEW_REFER_REL: [u8; 2] = [1, 4]; - pub(super) const THIRDP_REFER_REL: [u8; 2] = [1, 5]; - pub(super) const REL_CANCEL: [u8; 2] = [1, 255]; -} +const TSP_ETS_WRAPPER: u16 = cesr!("E"); +const TSP_S_WRAPPER: u16 = cesr!("S"); +const TSP_HOP_LIST: u16 = cesr!("J"); +const TSP_PAYLOAD: u16 = cesr!("Z"); + +const TSP_TMP: u32 = cesr!("X"); + +/// Constants for payload field types +// NOTE: this is for future extensibility +#[allow(unused)] +const XCTL: [u8; 3] = cesr_data("XCTL"); +const XSCS: [u8; 3] = cesr_data("XSCS"); +const XHOP: [u8; 3] = cesr_data("XHOP"); +#[allow(unused)] +const XPAD: [u8; 3] = cesr_data("XPAD"); +const XRFI: [u8; 3] = cesr_data("XRFI"); +const XRFA: [u8; 3] = cesr_data("XRFA"); +const XRFD: [u8; 3] = cesr_data("XRFD"); +const YTSP: [u8; 3] = cesr_data("YTSP"); + +// FIXME: a temporary code for third party referrals +const X3RR: [u8; 3] = cesr_data("X3RR"); + +// FIXME: a temporary code for nested relationships +const XRNI: [u8; 3] = cesr_data("XRNI"); +const XRNA: [u8; 3] = cesr_data("XRNA"); use super::{ decode::{ decode_count, decode_count_mut, decode_fixed_data, decode_fixed_data_mut, decode_variable_data, decode_variable_data_index, decode_variable_data_mut, + opt_decode_variable_data_mut, }, encode::{encode_count, encode_fixed_data}, error::{DecodeError, EncodeError}, @@ -96,7 +115,11 @@ pub enum Payload<'a, Bytes, Vid> { /// A TSP message confirming a relationship NestedRelationAffirm { message: Bytes, reply: Digest<'a> }, /// A TSP Message establishing a secondary relationship (parallel relationship forming) - NewIdentifierProposal { thread_id: Digest<'a>, new_vid: Vid }, + NewIdentifierProposal { + thread_id: Digest<'a>, + sig_thread_id: &'a Signature, + new_vid: Vid, + }, /// A TSP Message revealing a third party RelationshipReferral { referred_vid: Vid }, /// A TSP cancellation message @@ -123,7 +146,7 @@ impl, Vid: AsRef<[u8]>> Payload<'_, Bytes, Vid> { #[cfg(feature = "fuzzing")] pub mod fuzzing; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Copy)] #[repr(u8)] pub enum CryptoType { Plaintext = 0, @@ -152,30 +175,13 @@ impl AsCryptoType for kem::X25519HkdfSha256 { } } -impl TryFrom for CryptoType { - type Error = DecodeError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(CryptoType::Plaintext), - 1 => Ok(CryptoType::HpkeAuth), - 2 => Ok(CryptoType::HpkeEssr), - 3 => Ok(CryptoType::NaclAuth), - 4 => Ok(CryptoType::NaclEssr), - #[cfg(feature = "pq")] - 5 => Ok(CryptoType::X25519Kyber768Draft00), - _ => Err(DecodeError::InvalidCryptoType), - } - } -} - impl CryptoType { pub(crate) fn is_encrypted(&self) -> bool { !matches!(self, CryptoType::Plaintext) } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Copy)] #[repr(u8)] pub enum SignatureType { NoSignature = 0, @@ -184,17 +190,10 @@ pub enum SignatureType { MlDsa65 = 2, } -impl TryFrom for SignatureType { - type Error = DecodeError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(SignatureType::NoSignature), - 1 => Ok(SignatureType::Ed25519), - #[cfg(feature = "pq")] - 2 => Ok(SignatureType::MlDsa65), - _ => Err(DecodeError::InvalidSignatureType), - } +impl SignatureType { + #[allow(unused)] + pub(crate) fn is_signed(&self) -> bool { + !matches!(self, SignatureType::NoSignature) } } @@ -225,13 +224,7 @@ fn checked_encode_variable_data( const DATA_LIMIT: usize = 3 * (1 << 24); if payload.len() >= DATA_LIMIT { - // since blobs have no identifier, that information is lost on large payloads and a "blob" can only be used - // for TSP_PLAINTEXT or TSP_CIPHERTEXT. - if identifier == TSP_PLAINTEXT || identifier == TSP_CIPHERTEXT { - super::encode::encode_large_blob(payload, stream); - } else { - return Err(EncodeError::ExcessiveFieldSize); - } + return Err(EncodeError::ExcessiveFieldSize); } else { super::encode::encode_variable_data(identifier, payload, stream); } @@ -239,7 +232,7 @@ fn checked_encode_variable_data( Ok(()) } -/// Safely decode variable data, detecting blobs +/// Safely decode variable data fn checked_decode_variable_data_mut( identifier: u32, stream: &mut [u8], @@ -251,28 +244,13 @@ fn checked_decode_variable_data_mut( Some((slice, stream)) } -/// Safely decode variable data, detecting blobs +/// Safely decode variable data fn checked_decode_variable_data_index( identifier: u32, stream: &[u8], pos: &mut usize, ) -> Option> { - if let Some(result) = decode_variable_data_index(identifier, stream, pos) { - Some(result) - } else { - // since blobs have no identifier, that information is lost on large payloads and a "blob" can only be used - // for TSP_PLAINTEXT or TSP_CIPHERTEXT. - if identifier == TSP_PLAINTEXT || identifier == TSP_CIPHERTEXT { - let mut range = super::decode::decode_large_blob_index(&stream[*pos..])?; - range.start += *pos; - range.end += *pos; - *pos = range.end; - - Some(range) - } else { - None - } - } + decode_variable_data_index(identifier, stream, pos) } /// Encode a TSP Payload into CESR for encryption @@ -281,24 +259,24 @@ pub fn encode_payload( sender_identity: Option<&[u8]>, output: &mut impl for<'a> Extend<&'a u8>, ) -> Result<(), EncodeError> { + encode_count(TSP_PAYLOAD, 1usize, output); if let Some(sender_identity) = sender_identity { - encode_count(TSP_PAYLOAD, 2, output); - checked_encode_variable_data(TSP_DEVELOPMENT_VID, sender_identity, output)?; - } else { - encode_count(TSP_PAYLOAD, 1, output); + checked_encode_variable_data(TSP_VID, sender_identity, output)?; } match payload { Payload::GenericMessage(data) => { - encode_fixed_data(TSP_TYPECODE, &msgtype::GEN_MSG, output); + output.extend(&XSCS); checked_encode_variable_data(TSP_PLAINTEXT, data.as_ref(), output)?; } Payload::NestedMessage(data) => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEST_MSG, output); + output.extend(&XHOP); + let no_hops: [&[u8]; 0] = []; + encode_hops(&no_hops, output)?; checked_encode_variable_data(TSP_PLAINTEXT, data.as_ref(), output)?; } Payload::RoutedMessage(hops, data) => { - encode_fixed_data(TSP_TYPECODE, &msgtype::GEN_MSG, output); + output.extend(&XHOP); if hops.is_empty() { return Err(EncodeError::MissingHops); } @@ -306,19 +284,20 @@ pub fn encode_payload( checked_encode_variable_data(TSP_PLAINTEXT, data.as_ref(), output)?; } Payload::DirectRelationProposal { nonce, hops } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEW_REL, output); + output.extend(&XRFI); encode_hops(hops, output)?; encode_fixed_data(TSP_NONCE, &nonce.0, output); + checked_encode_variable_data(TSP_VID, &[], output)?; } Payload::DirectRelationAffirm { reply } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEW_REL_REPLY, output); + output.extend(&XRFA); encode_digest(reply, output); } Payload::NestedRelationProposal { message: data, nonce, } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEW_NEST_REL, output); + output.extend(&XRNI); checked_encode_variable_data(TSP_PLAINTEXT, data.as_ref(), output)?; encode_fixed_data(TSP_NONCE, &nonce.0, output); } @@ -326,21 +305,32 @@ pub fn encode_payload( message: data, reply, } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEW_NEST_REL_REPLY, output); + output.extend(&XRNA); checked_encode_variable_data(TSP_PLAINTEXT, data.as_ref(), output)?; encode_digest(reply, output); } - Payload::NewIdentifierProposal { thread_id, new_vid } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::NEW_REFER_REL, output); + Payload::NewIdentifierProposal { + thread_id, + sig_thread_id, + new_vid, + } => { + if new_vid.as_ref().is_empty() { + return Err(EncodeError::InvalidVid); + } + output.extend(&XRFI); + let no_hops: [&[u8]; 0] = []; + encode_hops(&no_hops, output)?; + encode_fixed_data(TSP_NONCE, &[0; 32], output); // this does not need to be a secure nonce + checked_encode_variable_data(TSP_VID, new_vid.as_ref(), output)?; encode_digest(thread_id, output); - checked_encode_variable_data(TSP_DEVELOPMENT_VID, new_vid.as_ref(), output)?; + encode_fixed_data(ED25519_SIGNATURE, sig_thread_id, output); } Payload::RelationshipReferral { referred_vid } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::THIRDP_REFER_REL, output); - checked_encode_variable_data(TSP_DEVELOPMENT_VID, referred_vid.as_ref(), output)?; + output.extend(&X3RR); + checked_encode_variable_data(TSP_VID, referred_vid.as_ref(), output)?; } Payload::RelationshipCancel { reply } => { - encode_fixed_data(TSP_TYPECODE, &msgtype::REL_CANCEL, output); + output.extend(&XRFD); encode_digest(reply, output); } } @@ -353,11 +343,9 @@ pub fn encode_hops( hops: &[impl AsRef<[u8]>], output: &mut impl for<'a> Extend<&'a u8>, ) -> Result<(), EncodeError> { - if !hops.is_empty() { - encode_count(TSP_HOP_LIST, hops.len() as u16, output); - for hop in hops { - checked_encode_variable_data(TSP_DEVELOPMENT_VID, hop.as_ref(), output)?; - } + encode_count(TSP_HOP_LIST, hops.len() as u16, output); + for hop in hops { + checked_encode_variable_data(TSP_VID, hop.as_ref(), output)?; } Ok(()) @@ -367,20 +355,13 @@ pub fn encode_hops( fn decode_hops<'a, Vid: TryFrom<&'a [u8]>>( stream: &'a mut [u8], ) -> Result<(Vec, &'a mut [u8]), DecodeError> { - // a rare case of Rust's borrow checker not being able to figure out - // that a "None" isn't borrowing from anybody; so we have to call - // the referentially transparent decode_count_mut twice... - if decode_count_mut(TSP_HOP_LIST, stream).is_none() { - return Ok((Vec::new(), stream)); - } - - let (hop_length, mut stream) = decode_count_mut(TSP_HOP_LIST, stream).unwrap(); + let (hop_length, mut stream) = + decode_count_mut(TSP_HOP_LIST, stream).ok_or(DecodeError::MissingHops)?; let mut hop_list = Vec::with_capacity(hop_length as usize); for _ in 0..hop_length { let hop: &[u8]; - (hop, stream) = decode_variable_data_mut(TSP_DEVELOPMENT_VID, stream) - .ok_or(DecodeError::UnexpectedData)?; + (hop, stream) = decode_variable_data_mut(TSP_VID, stream).ok_or(DecodeError::VidError)?; hop_list.push(hop.try_into().map_err(|_| DecodeError::VidError)?); } @@ -420,67 +401,77 @@ pub fn encode_digest(digest: &Digest, output: &mut impl for<'a> Extend<&'a u8>) /// Decode a TSP Payload pub fn decode_payload(mut stream: &mut [u8]) -> Result, DecodeError> { - let sender_identity = match decode_count_mut(TSP_PAYLOAD, stream) { - Some((2, upd_stream)) => { - let essr_prefix: &[u8]; - (essr_prefix, stream) = decode_variable_data_mut(TSP_DEVELOPMENT_VID, upd_stream) - .ok_or(DecodeError::UnexpectedData)?; - - Some(essr_prefix) - } - Some((1, upd_stream)) => { - stream = upd_stream; + //NOTE: we do not need the quadlet count + let _count; + (_count, stream) = decode_count_mut(TSP_PAYLOAD, stream).ok_or(DecodeError::UnexpectedData)?; - None - } - _ => return Err(DecodeError::VersionMismatch), - }; + let sender_identity; + (sender_identity, stream) = opt_decode_variable_data_mut(TSP_VID, stream); - let (&mut msgtype, mut stream) = - decode_fixed_data_mut(TSP_TYPECODE, stream).ok_or(DecodeError::UnexpectedData)?; + let (msgtype, mut stream) = stream + .split_at_mut_checked(3) + .ok_or(DecodeError::UnexpectedData)?; - let payload = match msgtype { - msgtype::GEN_MSG => { - let (hop_list, upd_stream) = decode_hops(stream)?; + let payload = match *<&[u8; 3]>::try_from(msgtype as &[u8]).unwrap() { + XSCS => { let msg; + (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, stream) + .ok_or(DecodeError::UnexpectedData)?; + + Payload::GenericMessage(msg) + } + XHOP => { + let (hop_list, msg); + (hop_list, stream) = decode_hops(stream)?; if hop_list.is_empty() { - (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, upd_stream) + (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, stream) .ok_or(DecodeError::UnexpectedData)?; - Payload::GenericMessage(msg) + Payload::NestedMessage(msg) } else { - (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, upd_stream) + (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, stream) .ok_or(DecodeError::UnexpectedData)?; Payload::RoutedMessage(hop_list, msg) } } - msgtype::NEW_REL => { - let (hop_list, upd_stream) = decode_hops(stream)?; + XRFI => { + let hop_list; + (hop_list, stream) = decode_hops(stream)?; let nonce; (nonce, stream) = - decode_fixed_data_mut(TSP_NONCE, upd_stream).ok_or(DecodeError::UnexpectedData)?; + decode_fixed_data_mut(TSP_NONCE, stream).ok_or(DecodeError::UnexpectedData)?; - Payload::DirectRelationProposal { - nonce: Nonce(*nonce), - hops: hop_list, - } - } - msgtype::NEST_MSG => { - let msg; - (msg, stream) = checked_decode_variable_data_mut(TSP_PLAINTEXT, stream) - .ok_or(DecodeError::UnexpectedData)?; + let new_vid: &[u8]; + (new_vid, stream) = + decode_variable_data_mut(TSP_VID, stream).ok_or(DecodeError::UnexpectedData)?; + + if new_vid.is_empty() { + Payload::DirectRelationProposal { + nonce: Nonce(*nonce), + hops: hop_list, + } + } else { + let (thread_id, sig_thread_id); + (thread_id, stream) = decode_digest(stream)?; + (sig_thread_id, stream) = decode_fixed_data_mut::<64>(ED25519_SIGNATURE, stream) + .ok_or(DecodeError::UnexpectedData)?; - Payload::NestedMessage(msg) + Payload::NewIdentifierProposal { + thread_id, + sig_thread_id, + new_vid, + } + } } - msgtype::NEW_REL_REPLY => { + XRFA => { let reply; (reply, stream) = decode_digest(stream)?; Payload::DirectRelationAffirm { reply } } - msgtype::NEW_NEST_REL => { + XRNI => { let data: &mut [u8]; (data, stream) = decode_variable_data_mut(TSP_PLAINTEXT, stream) .ok_or(DecodeError::UnexpectedData)?; @@ -494,7 +485,7 @@ pub fn decode_payload(mut stream: &mut [u8]) -> Result, Decod nonce: Nonce(*nonce), } } - msgtype::NEW_NEST_REL_REPLY => { + XRNA => { let data: &mut [u8]; let reply; (data, stream) = decode_variable_data_mut(TSP_PLAINTEXT, stream) @@ -506,22 +497,14 @@ pub fn decode_payload(mut stream: &mut [u8]) -> Result, Decod reply, } } - msgtype::NEW_REFER_REL => { - let (thread_id, upd_stream) = decode_digest(stream)?; - let new_vid: &[u8]; - (new_vid, stream) = decode_variable_data_mut(TSP_DEVELOPMENT_VID, upd_stream) - .ok_or(DecodeError::UnexpectedData)?; - - Payload::NewIdentifierProposal { thread_id, new_vid } - } - msgtype::THIRDP_REFER_REL => { + X3RR => { let referred_vid: &[u8]; - (referred_vid, stream) = decode_variable_data_mut(TSP_DEVELOPMENT_VID, stream) - .ok_or(DecodeError::UnexpectedData)?; + (referred_vid, stream) = + decode_variable_data_mut(TSP_VID, stream).ok_or(DecodeError::UnexpectedData)?; Payload::RelationshipReferral { referred_vid } } - msgtype::REL_CANCEL => { + XRFD => { let reply; (reply, stream) = decode_digest(stream)?; @@ -540,12 +523,42 @@ pub fn decode_payload(mut stream: &mut [u8]) -> Result, Decod } } +const fn encoded_version() -> u16 { + (TSP_VERSION.1 as u16) << 6 | (TSP_VERSION.2 as u16) +} + +/// Encode a TSP version marker +pub fn encode_version(output: &mut impl for<'b> Extend<&'b u8>) { + output.extend(&YTSP); + encode_count(TSP_VERSION.0, encoded_version(), output); +} + +fn decode_version(stream: &mut &[u8]) -> Result<(), DecodeError> { + // See above: this is hopefully rare case of pseudo-CESR encoding + let Some((hdr, new_stream)) = stream.split_at_checked(YTSP.len()) else { + return Err(DecodeError::VersionMismatch); + }; + + if hdr != YTSP { + return Err(DecodeError::VersionMismatch); + } + + *stream = new_stream; + + let _version = decode_count(TSP_VERSION.0, stream).ok_or(DecodeError::VersionMismatch)?; + + // NOTE: can we simply ignore the minor and path parts of the version? + + Ok(()) +} + /// Encode a encrypted TSP message plus Envelope into CESR pub fn encode_ets_envelope<'a, Vid: AsRef<[u8]>>( envelope: Envelope<'a, Vid>, output: &mut impl for<'b> Extend<&'b u8>, ) -> Result<(), EncodeError> { - encode_count(TSP_ETS_WRAPPER, 1, output); + // TODO: we don't know the size yet + encode_count(TSP_ETS_WRAPPER, 1usize, output); encode_envelope_fields(envelope, output) } @@ -554,8 +567,7 @@ pub fn encode_s_envelope<'a, Vid: AsRef<[u8]>>( envelope: Envelope<'a, Vid>, output: &mut impl for<'b> Extend<&'b u8>, ) -> Result<(), EncodeError> { - encode_count(TSP_S_WRAPPER, 1, output); - + encode_count(TSP_S_WRAPPER, 1usize, output); encode_envelope_fields(envelope, output) } @@ -566,18 +578,16 @@ fn encode_envelope_fields<'a, Vid: AsRef<[u8]>>( envelope: Envelope<'a, Vid>, output: &mut impl for<'b> Extend<&'b u8>, ) -> Result<(), EncodeError> { - encode_fixed_data(TSP_TYPECODE, &[0, 0], output); - encode_fixed_data( - TSP_TYPECODE, - &[envelope.crypto_type as u8, envelope.signature_type as u8], - output, - ); - checked_encode_variable_data(TSP_DEVELOPMENT_VID, envelope.sender.as_ref(), output)?; + encode_version(output); + checked_encode_variable_data(TSP_VID, envelope.sender.as_ref(), output)?; if let Some(rec) = envelope.receiver { - checked_encode_variable_data(TSP_DEVELOPMENT_VID, rec.as_ref(), output)?; + checked_encode_variable_data(TSP_VID, rec.as_ref(), output)?; } + // FIXME: without this parsing errors seem to occur -- maybe there is am ambiguity + encode_fixed_data(TSP_TMP, &[0u8, 0u8], output); + if let Some(data) = envelope.nonconfidential_data { checked_encode_variable_data(TSP_PLAINTEXT, data, output)?; } @@ -603,49 +613,115 @@ pub fn encode_signature( } } +impl CryptoType { + fn cesr_code(&self) -> Result { + Ok(match self { + CryptoType::NaclEssr => TSP_NACL_CIPHERTEXT, + CryptoType::HpkeEssr => TSP_HPKEBASE_CIPHERTEXT, + CryptoType::HpkeAuth => TSP_HPKEAUTH_CIPHERTEXT, + CryptoType::NaclAuth => TSP_NACLAUTH_CIPHERTEXT, + #[cfg(feature = "pq")] + CrytpoType::X25519Kyber768Draft00 => TSP_HPKEPQ_CIPHERTEXT, + _ => return Err(DecodeError::InvalidCrypto), + }) + } +} + /// Encode a encrypted ciphertext into CESR pub fn encode_ciphertext( ciphertext: &[u8], + crypto: CryptoType, output: &mut impl for<'a> Extend<&'a u8>, ) -> Result<(), EncodeError> { - checked_encode_variable_data(TSP_CIPHERTEXT, ciphertext, output) + checked_encode_variable_data(crypto.cesr_code().unwrap(), ciphertext, output) } /// Checks whether the expected TSP header is present and returns its size and whether it /// is a "ETS" or "S" envelope +#[allow(clippy::type_complexity)] pub(super) fn detected_tsp_header_size_and_confidentiality( - stream: &mut &[u8], -) -> Result<(usize, CryptoType, SignatureType), DecodeError> { - let origin = stream as &[u8]; - let encrypted = if let Some(1) = decode_count(TSP_ETS_WRAPPER, stream) { + stream: &[u8], + pos: &mut usize, +) -> Result< + ( + Range, + Option>, + CryptoType, + SignatureType, + ), + DecodeError, +> { + let origin = stream; + let mut stream = &origin[*pos..]; + //NOTE: we don't need this quadlet count + let encrypted = if let Some(_quadlet_count) = decode_count(TSP_ETS_WRAPPER, &mut stream) { true - } else if let Some(1) = decode_count(TSP_S_WRAPPER, stream) { + } else if let Some(1) = decode_count(TSP_S_WRAPPER, &mut stream) { false } else { return Err(DecodeError::VersionMismatch); }; - match decode_fixed_data(TSP_TYPECODE, stream) { - Some([0, 0]) => {} - _ => return Err(DecodeError::VersionMismatch), - } + decode_version(&mut stream)?; + let mut mid_pos = *pos + origin.len() - stream.len(); - let (crypto_type, signature_type) = match decode_fixed_data(TSP_TYPECODE, stream) { - Some([crypto, signature]) => { - let crypto_type = CryptoType::try_from(*crypto)?; + let sender = decode_variable_data_index(TSP_VID, origin, &mut mid_pos) + .ok_or(DecodeError::UnexpectedData)?; - if crypto_type.is_encrypted() != encrypted { - return Err(DecodeError::VersionMismatch); - } + let receiver = decode_variable_data_index(TSP_VID, origin, &mut mid_pos); + + let mut stream = &origin[mid_pos..]; + + if let Some([_crypto_type, _signature_type]) = decode_fixed_data(TSP_TMP, &mut stream) {} - (crypto_type, SignatureType::try_from(*signature)?) + *pos += origin.len() - stream.len(); + + /* look ahead to determine the crypto and signature types */ + let _nonconf_data = decode_variable_data(TSP_PLAINTEXT, &mut stream); + + let crypto_type = if decode_variable_data(TSP_HPKEAUTH_CIPHERTEXT, &mut stream).is_some() { + CryptoType::HpkeAuth + } else if decode_variable_data(TSP_HPKEBASE_CIPHERTEXT, &mut stream).is_some() { + CryptoType::HpkeEssr + } else if decode_variable_data(TSP_NACL_CIPHERTEXT, &mut stream).is_some() { + CryptoType::NaclEssr + } else if decode_variable_data(TSP_NACLAUTH_CIPHERTEXT, &mut stream).is_some() { + CryptoType::NaclAuth + } else { + #[cfg(feature = "pq")] + if decode_variable_data(TSP_HPKEPQ_CIPHERTEXT, &mut stream).is_some() { + CryptoType::X25519Kyber768Draft00 + } else if encrypted { + return Err(DecodeError::UnknownCrypto); + } else { + CryptoType::Plaintext + } + #[cfg(not(feature = "pq"))] + if encrypted { + return Err(DecodeError::UnknownCrypto); + } else { + CryptoType::Plaintext } - _ => return Err(DecodeError::VersionMismatch), }; - debug_assert_eq!(origin.len() - stream.len(), 9); + if encrypted != crypto_type.is_encrypted() { + return Err(DecodeError::InvalidCrypto); + } + + let signature_type = if decode_fixed_data::<64>(ED25519_SIGNATURE, &mut stream).is_some() { + SignatureType::Ed25519 + } else { + #[cfg(feature = "pq")] + if code_fixed_data::<3309>(ML_DSA_65_SIGNATURE, &mut stream).is_some() { + SignatureType::MlDsa65 + } else { + SignatureType::NoSignature + } + #[cfg(not(feature = "pq"))] + SignatureType::NoSignature + }; - Ok((9, crypto_type, signature_type)) + Ok((sender, receiver, crypto_type, signature_type)) } /// A structure representing a siganture + data that needs to be verified. @@ -659,17 +735,18 @@ pub struct VerificationChallenge<'a> { /// Decode the type, sender and receiver of an encrypted TSP message pub fn decode_sender_receiver<'a, Vid: TryFrom<&'a [u8]>>( - stream: &mut &'a [u8], + stream: &'a [u8], ) -> Result<(Vid, Option, CryptoType, SignatureType), DecodeError> { - let (_, crypto_type, signature_type) = detected_tsp_header_size_and_confidentiality(stream)?; + let mut pos = 0; + let (sender, receiver, crypto_type, signature_type) = + detected_tsp_header_size_and_confidentiality(stream, &mut pos)?; - let sender = decode_variable_data(TSP_DEVELOPMENT_VID, stream) - .ok_or(DecodeError::UnexpectedData)? + let sender = stream[sender] .try_into() .map_err(|_| DecodeError::VidError)?; - let receiver = decode_variable_data(TSP_DEVELOPMENT_VID, stream) - .map(|r| r.try_into().map_err(|_| DecodeError::VidError)) + let receiver = receiver + .map(|r| stream[r].try_into().map_err(|_| DecodeError::VidError)) .transpose()?; Ok((sender, receiver, crypto_type, signature_type)) @@ -753,20 +830,16 @@ impl<'a> CipherView<'a> { } pub(crate) fn signature_type(&self) -> SignatureType { - self.signature_type.clone() + self.signature_type } } /// Decode an encrypted TSP message plus Envelope & Signature /// Produces the ciphertext as a mutable stream. pub fn decode_envelope<'a>(stream: &'a mut [u8]) -> Result, DecodeError> { - let (mut pos, crypto_type, signature_type) = - detected_tsp_header_size_and_confidentiality(&mut (stream as &[u8]))?; - - let sender = decode_variable_data_index(TSP_DEVELOPMENT_VID, stream, &mut pos) - .ok_or(DecodeError::UnexpectedData)?; - - let receiver = decode_variable_data_index(TSP_DEVELOPMENT_VID, stream, &mut pos); + let mut pos = 0; + let (sender, receiver, crypto_type, signature_type) = + detected_tsp_header_size_and_confidentiality(stream, &mut pos)?; let nonconfidential_data = decode_variable_data_index(TSP_PLAINTEXT, stream, &mut pos); @@ -774,7 +847,7 @@ pub fn decode_envelope<'a>(stream: &'a mut [u8]) -> Result, Decod let ciphertext = if crypto_type.is_encrypted() { Some( - checked_decode_variable_data_index(TSP_CIPHERTEXT, stream, &mut pos) + checked_decode_variable_data_index(crypto_type.cesr_code()?, stream, &mut pos) .ok_or(DecodeError::UnexpectedData)?, ) } else { @@ -897,18 +970,25 @@ pub struct MessageParts<'a> { /// Decode a CESR-encoded message into its CESR-encoded parts pub fn open_message_into_parts(data: &[u8]) -> Result, DecodeError> { - let (mut pos, crypto_type, signature_type) = - detected_tsp_header_size_and_confidentiality(&mut (data as &[u8]))?; + let mut pos = 0; + let (sender, receiver, crypto_type, signature_type) = + detected_tsp_header_size_and_confidentiality(data, &mut pos)?; let prefix = Part { prefix: &data[..pos], data: &[], }; - let sender = Part::decode(TSP_DEVELOPMENT_VID, data, &mut pos).ok_or(DecodeError::VidError)?; - let receiver = Part::decode(TSP_DEVELOPMENT_VID, data, &mut pos); + let sender = Part { + prefix: &[], + data: &data[sender], + }; + let receiver = receiver.map(|r| Part { + prefix: &[], + data: &data[r], + }); let nonconfidential_data = Part::decode(TSP_PLAINTEXT, data, &mut pos); - let ciphertext = Part::decode(TSP_CIPHERTEXT, data, &mut pos); + let ciphertext = Part::decode(crypto_type.cesr_code()?, data, &mut pos); let signature: &[u8; 64] = decode_fixed_data(ED25519_SIGNATURE, &mut &data[pos..]) .ok_or(DecodeError::SignatureError)?; @@ -1048,7 +1128,7 @@ mod test { }) .unwrap(); let ciphertext = dummy_crypt(&mut cesr_payload); - encode_ciphertext(ciphertext, &mut outer).unwrap(); + encode_ciphertext(ciphertext, CryptoType::HpkeAuth, &mut outer).unwrap(); let signed_data = outer.clone(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); @@ -1096,7 +1176,7 @@ mod test { }) .unwrap(); let ciphertext = dummy_crypt(&mut cesr_payload); - encode_ciphertext(ciphertext, &mut outer).unwrap(); + encode_ciphertext(ciphertext, CryptoType::HpkeAuth, &mut outer).unwrap(); let signed_data = outer.clone(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); @@ -1177,7 +1257,7 @@ mod test { }) .unwrap(); let ciphertext = dummy_crypt(&cesr_payload); // this is wrong - encode_ciphertext(ciphertext, &mut outer).unwrap(); + encode_ciphertext(ciphertext, CryptoType::HpkeAuth, &mut outer).unwrap(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); assert!(decode_envelope(&mut outer).is_err()); @@ -1201,7 +1281,7 @@ mod test { ) .unwrap(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); - encode_ciphertext(&[], &mut outer).unwrap(); + encode_ciphertext(&[], CryptoType::HpkeAuth, &mut outer).unwrap(); assert!(decode_envelope(&mut outer).is_err()); } @@ -1219,7 +1299,7 @@ mod test { nonconfidential_data: Some(b"treasure"), }) .unwrap(); - encode_ciphertext(&[], &mut outer).unwrap(); + encode_ciphertext(&[], CryptoType::HpkeAuth, &mut outer).unwrap(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); outer.push(b'-'); @@ -1286,6 +1366,7 @@ mod test { fn test_par_refer_rel() { test_turn_around(Payload::NewIdentifierProposal { thread_id: Digest::Sha2_256(&Default::default()), + sig_thread_id: &[5; 64], new_vid: b"Charlie", }); } @@ -1315,7 +1396,7 @@ mod test { }) .unwrap(); let ciphertext = dummy_crypt(&mut cesr_payload); - encode_ciphertext(ciphertext, &mut outer).unwrap(); + encode_ciphertext(ciphertext, CryptoType::HpkeAuth, &mut outer).unwrap(); let signed_data = outer.clone(); encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); @@ -1377,37 +1458,57 @@ mod test { }); } + #[ignore] #[test] #[wasm_bindgen_test] fn test_message_to_parts() { use base64ct::{Base64UrlUnpadded, Encoding}; - let message = Base64UrlUnpadded::decode_vec("-EABXAAAXAEB9VIDAAAEZGlkOnRlc3Q6Ym9i8VIDAAAFAGRpZDp0ZXN0OmFsaWNl6BAEAABleHRyYSBkYXRh4CAXScvzIiBCgfOu9jHtGwd1qN-KlMB7uhFbE9YOSyTmnp9yziA1LVPdQmST27yjuDRTlxeRo7H7gfuaGFY4iyf2EsfiqvEg0BBNDbKoW0DDczGxj7rNWKH_suyj18HCUxMZ6-mDymZdNhHZIS8zIstC9Kxv5Q-GxmI-1v4SNbeCemuCMBzMPogK").unwrap(); + let message = Base64UrlUnpadded::decode_vec("-EABYTSP-AABXAAAXAEB6VAEZGlkOnRlc3Q6Ym9i8VIDAAAFAGRpZDp0ZXN0OmFsaWNl6BAEAABleHRyYSBkYXRh4CAXScvzIiBCgfOu9jHtGwd1qN-KlMB7uhFbE9YOSyTmnp9yziA1LVPdQmST27yjuDRTlxeRo7H7gfuaGFY4iyf2EsfiqvEg0BBNDbKoW0DDczGxj7rNWKH_suyj18HCUxMZ6-mDymZdNhHZIS8zIstC9Kxv5Q-GxmI-1v4SNbeCemuCMBzMPogK").unwrap(); let parts = open_message_into_parts(&message).unwrap(); - assert_eq!(parts.prefix.prefix.len(), 9); + assert_eq!(parts.prefix.prefix.len(), 15); assert_eq!(parts.sender.data.len(), 10); assert_eq!(parts.receiver.unwrap().data.len(), 14); assert_eq!(parts.ciphertext.unwrap().data.len(), 69); } #[test] - fn test_blob() { - let payload = vec![b'M'; 50]; - let mut data = vec![]; - checked_encode_variable_data(TSP_PLAINTEXT, &payload, &mut data).unwrap(); - let input = &mut data[..]; - let (source, _) = decode_variable_data_mut(TSP_PLAINTEXT, input).unwrap(); - assert!(source.len() == 50); - let (source, _) = checked_decode_variable_data_mut(TSP_PLAINTEXT, input).unwrap(); - assert!(source.len() == 50); - - let payload = vec![b'M'; 60_000_000]; - let mut data = vec![]; - checked_encode_variable_data(TSP_PLAINTEXT, &payload, &mut data).unwrap(); - let input = &mut data[..]; - assert!(decode_variable_data_mut(TSP_PLAINTEXT, input).is_none()); - let (source, _) = checked_decode_variable_data_mut(TSP_PLAINTEXT, input).unwrap(); - assert!(source.len() == 60_000_000); + fn test_decode_send_recv() { + fn dummy_crypt(data: &mut [u8]) -> &mut [u8] { + data + } + let fixed_sig = [1; 64]; + + let mut cesr_payload = + { encode_payload_vec(&Payload::<_, &[u8]>::GenericMessage(b"Hello TSP!")).unwrap() }; + + let mut outer = encode_ets_envelope_vec(Envelope { + crypto_type: CryptoType::HpkeAuth, + signature_type: SignatureType::Ed25519, + sender: &b"Alister"[..], + receiver: Some(&b"Bobbi"[..]), + nonconfidential_data: None, + }) + .unwrap(); + let ciphertext = dummy_crypt(&mut cesr_payload); + encode_ciphertext(ciphertext, CryptoType::HpkeAuth, &mut outer).unwrap(); + + let signed_data = outer.clone(); + encode_signature(&fixed_sig, &mut outer, SignatureType::Ed25519); + + let outer2 = outer.clone(); + let view = decode_envelope(&mut outer).unwrap(); + let ver = view.as_challenge(); + assert_eq!(ver.signed_data, signed_data); + assert_eq!(ver.signature, &fixed_sig); + let DecodedEnvelope { envelope: env, .. } = view.into_opened().unwrap(); + assert_eq!(env.sender, &b"Alister"[..]); + assert_eq!(env.receiver, Some(&b"Bobbi"[..])); + assert_eq!(env.nonconfidential_data, None); + + let (sender, receiver, _, _) = decode_sender_receiver(&outer2).unwrap(); + assert_eq!(env.sender, sender); + assert_eq!(env.receiver, receiver); } } diff --git a/tsp_sdk/src/cesr/packet/fuzzing.rs b/tsp_sdk/src/cesr/packet/fuzzing.rs index ec6b871d..d4f72b4b 100644 --- a/tsp_sdk/src/cesr/packet/fuzzing.rs +++ b/tsp_sdk/src/cesr/packet/fuzzing.rs @@ -79,6 +79,7 @@ impl<'a> arbitrary::Arbitrary<'a> for Wrapper { Variants::NewIdentifierProposal => Payload::NewIdentifierProposal { thread_id: digest(&DIGEST), new_vid: Arbitrary::arbitrary(u)?, + sig_thread_id: &[42; 64], }, Variants::RelationshipReferral => Payload::RelationshipReferral { referred_vid: Arbitrary::arbitrary(u)?, @@ -138,10 +139,12 @@ impl<'a> PartialEq> for Wrapper { Payload::NewIdentifierProposal { new_vid: l_vid, thread_id: l_reply, + sig_thread_id: _l_sig, }, Payload::NewIdentifierProposal { new_vid: r_vid, thread_id: r_reply, + sig_thread_id: _r_sig, }, ) => l_vid == r_vid && l_reply == r_reply, ( diff --git a/tsp_sdk/src/crypto/tsp_hpke.rs b/tsp_sdk/src/crypto/tsp_hpke.rs index a660ad23..72e331d2 100644 --- a/tsp_sdk/src/crypto/tsp_hpke.rs +++ b/tsp_sdk/src/crypto/tsp_hpke.rs @@ -54,9 +54,11 @@ where VidSignatureKeyType::MlDsa65 => SignatureType::MlDsa65, }; + let crypto_type = Kem::crypto_type(); + crate::cesr::encode_ets_envelope( crate::cesr::Envelope { - crypto_type: Kem::crypto_type(), + crypto_type, signature_type, sender: sender.identifier(), receiver: Some(receiver.identifier()), @@ -102,7 +104,10 @@ where ref thread_id, new_vid, } => crate::cesr::Payload::NewIdentifierProposal { + //TODO: we need to produce a signature here with `new_vid`, but we don't have the PrivateVid for it at this point and that + //cannot be done without changing the "upper" API. thread_id: crate::cesr::Digest::Sha2_256(thread_id), + sig_thread_id: &[0; 64], new_vid, }, Payload::Referral { referred_vid } => { @@ -162,7 +167,7 @@ where cesr_message.extend(encapped_key.to_bytes()); // encode and append the ciphertext to the envelope data - crate::cesr::encode_ciphertext(&cesr_message, &mut data)?; + crate::cesr::encode_ciphertext(&cesr_message, crypto_type, &mut data)?; // create and append signature match sender.signature_key_type() { @@ -278,12 +283,16 @@ where }, crate::cesr::Payload::NestedMessage(data) => Payload::NestedMessage(data), crate::cesr::Payload::RoutedMessage(hops, data) => Payload::RoutedMessage(hops, data as _), - crate::cesr::Payload::NewIdentifierProposal { thread_id, new_vid } => { - Payload::NewIdentifier { - thread_id: *thread_id.as_bytes(), - new_vid, - } - } + crate::cesr::Payload::NewIdentifierProposal { + thread_id, + sig_thread_id: _, + new_vid, + } => Payload::NewIdentifier { + //TODO: the 'sig_thread_id' verification cannot happen at this point, and needs to be bubbled upward + //so it can be checked *after* the VID has been verified and key material retrieved. + thread_id: *thread_id.as_bytes(), + new_vid, + }, crate::cesr::Payload::RelationshipReferral { referred_vid } => { Payload::Referral { referred_vid } } diff --git a/tsp_sdk/src/crypto/tsp_nacl.rs b/tsp_sdk/src/crypto/tsp_nacl.rs index 43f28674..11233dbc 100644 --- a/tsp_sdk/src/crypto/tsp_nacl.rs +++ b/tsp_sdk/src/crypto/tsp_nacl.rs @@ -32,6 +32,9 @@ pub(crate) fn seal( let mut data = Vec::with_capacity(64); crate::cesr::encode_ets_envelope( crate::cesr::Envelope { + #[cfg(feature = "essr")] + crypto_type: CryptoType::NaclEssr, + #[cfg(not(feature = "essr"))] crypto_type: CryptoType::NaclAuth, signature_type: SignatureType::Ed25519, sender: sender.identifier(), @@ -73,7 +76,10 @@ pub(crate) fn seal( ref thread_id, new_vid, } => crate::cesr::Payload::NewIdentifierProposal { + //TODO: we need to produce a signature here with `new_vid`, but we don't have the PrivateVid for it at this point and that + //cannot be done without changing the "upper" API. thread_id: crate::cesr::Digest::Blake2b256(thread_id), + sig_thread_id: &[0; 64], new_vid, }, Payload::Referral { referred_vid } => { @@ -119,7 +125,15 @@ pub(crate) fn seal( cesr_message.extend(nonce); // encode and append the ciphertext to the envelope data - crate::cesr::encode_ciphertext(&cesr_message, &mut data)?; + crate::cesr::encode_ciphertext( + &cesr_message, + if cfg!(feature = "essr") { + CryptoType::NaclEssr + } else { + CryptoType::NaclAuth + }, + &mut data, + )?; // create and append signature match sender.signature_key_type() { @@ -200,12 +214,16 @@ pub(crate) fn open<'a>( inner, thread_id: *reply.as_bytes(), }, - crate::cesr::Payload::NewIdentifierProposal { thread_id, new_vid } => { - Payload::NewIdentifier { - thread_id: *thread_id.as_bytes(), - new_vid, - } - } + crate::cesr::Payload::NewIdentifierProposal { + thread_id, + sig_thread_id: _, + new_vid, + } => Payload::NewIdentifier { + //TODO: the sig_thread_id cannot be verified at this point, so needs to be bubbled upwards so it can be checked + //*after* the new VID has been retrieved and verified. + thread_id: *thread_id.as_bytes(), + new_vid, + }, crate::cesr::Payload::RelationshipReferral { referred_vid } => { Payload::Referral { referred_vid } }