[Fix] Harden MTP codec, transport, and SDK security
This commit is contained in:
parent
188caf56cc
commit
a7e804c603
73 changed files with 11892 additions and 5756 deletions
|
|
@ -6,6 +6,8 @@ pub enum CryptoError {
|
|||
EncryptionFailed,
|
||||
#[error("decryption failed")]
|
||||
DecryptionFailed,
|
||||
#[error("decryption output exceeds the caller's allocation limit")]
|
||||
AllocationLimit,
|
||||
#[error("malformed encryption envelope")]
|
||||
MalformedEnvelope,
|
||||
#[error("no encryption recipients")]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,114 @@ pub struct MultiEncryptedMessage {
|
|||
pub ciphertext: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Borrowed view of a canonical encrypted envelope.
|
||||
///
|
||||
/// The codec uses this view while validating an attacker-controlled envelope
|
||||
/// so parsing it does not first create a complete temporary copy of every
|
||||
/// recipient entry and the ciphertext.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MultiEncryptedMessageRef<'a> {
|
||||
encryption_type: EncryptionType,
|
||||
purpose: u8,
|
||||
bytes: &'a [u8],
|
||||
entries_start: usize,
|
||||
entry_len: usize,
|
||||
count: usize,
|
||||
ciphertext_start: usize,
|
||||
}
|
||||
|
||||
impl<'a> MultiEncryptedMessageRef<'a> {
|
||||
pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, CryptoError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
let encryption_type =
|
||||
EncryptionType::from_byte(bytes[0]).ok_or(CryptoError::UnknownAlgorithm)?;
|
||||
let purpose = bytes[1];
|
||||
let count = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
|
||||
if count == 0 || count > MAX_RECIPIENTS {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
let entry_len = encryption_type
|
||||
.kem_ciphertext_len()
|
||||
.checked_add(encryption_type.wrapped_key_len())
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
let entries_len = count
|
||||
.checked_mul(entry_len)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
let entries_start = 4usize;
|
||||
let ciphertext_start = entries_start
|
||||
.checked_add(entries_len)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
let ciphertext_len = bytes
|
||||
.len()
|
||||
.checked_sub(ciphertext_start)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
if ciphertext_len < encryption_type.minimum_ciphertext_len() {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
Ok(Self {
|
||||
encryption_type,
|
||||
purpose,
|
||||
bytes,
|
||||
entries_start,
|
||||
entry_len,
|
||||
count,
|
||||
ciphertext_start,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn encryption_type(&self) -> EncryptionType {
|
||||
self.encryption_type
|
||||
}
|
||||
|
||||
pub const fn purpose(&self) -> u8 {
|
||||
self.purpose
|
||||
}
|
||||
|
||||
pub const fn recipient_count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
pub fn recipient(&self, index: usize) -> Option<(&'a [u8], &'a [u8])> {
|
||||
if index >= self.count {
|
||||
return None;
|
||||
}
|
||||
let offset = self
|
||||
.entries_start
|
||||
.checked_add(index.checked_mul(self.entry_len)?)?;
|
||||
let kem_len = self.encryption_type.kem_ciphertext_len();
|
||||
let kem_end = offset.checked_add(kem_len)?;
|
||||
let end = offset.checked_add(self.entry_len)?;
|
||||
Some((
|
||||
self.bytes.get(offset..kem_end)?,
|
||||
self.bytes.get(kem_end..end)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn ciphertext(&self) -> &'a [u8] {
|
||||
&self.bytes[self.ciphertext_start..]
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> MultiEncryptedMessage {
|
||||
let recipients = (0..self.count)
|
||||
.filter_map(|index| {
|
||||
let (kem_ciphertext, encrypted_key) = self.recipient(index)?;
|
||||
Some(RecipientEntry {
|
||||
kem_ciphertext: kem_ciphertext.to_vec(),
|
||||
encrypted_key: encrypted_key.to_vec(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
MultiEncryptedMessage {
|
||||
encryption_type: self.encryption_type,
|
||||
purpose: self.purpose,
|
||||
recipients,
|
||||
ciphertext: self.ciphertext().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiEncryptedMessage {
|
||||
/// Serialize the envelope body without redundant per-recipient lengths.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, CryptoError> {
|
||||
|
|
@ -72,50 +180,7 @@ impl MultiEncryptedMessage {
|
|||
|
||||
/// Parse the canonical envelope body.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
let encryption_type =
|
||||
EncryptionType::from_byte(bytes[0]).ok_or(CryptoError::UnknownAlgorithm)?;
|
||||
let purpose = bytes[1];
|
||||
let count = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
|
||||
if count == 0 || count > MAX_RECIPIENTS {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
let entry_len = encryption_type.kem_ciphertext_len() + encryption_type.wrapped_key_len();
|
||||
let entries_len = count
|
||||
.checked_mul(entry_len)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
let start = 4usize;
|
||||
let end = start
|
||||
.checked_add(entries_len)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
let ciphertext_len = bytes
|
||||
.len()
|
||||
.checked_sub(end)
|
||||
.ok_or(CryptoError::MalformedEnvelope)?;
|
||||
if ciphertext_len < encryption_type.minimum_ciphertext_len() {
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
|
||||
let mut offset = start;
|
||||
let mut recipients = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let kem_end = offset + encryption_type.kem_ciphertext_len();
|
||||
let wrapped_end = kem_end + encryption_type.wrapped_key_len();
|
||||
recipients.push(RecipientEntry {
|
||||
kem_ciphertext: bytes[offset..kem_end].to_vec(),
|
||||
encrypted_key: bytes[kem_end..wrapped_end].to_vec(),
|
||||
});
|
||||
offset = wrapped_end;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
encryption_type,
|
||||
purpose,
|
||||
recipients,
|
||||
ciphertext: bytes[offset..].to_vec(),
|
||||
})
|
||||
Ok(MultiEncryptedMessageRef::from_bytes(bytes)?.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,20 +262,51 @@ pub fn decrypt_multi_for(
|
|||
purpose: u8,
|
||||
keyring: &Keyring,
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if message.recipients.is_empty()
|
||||
|| message.recipients.len() > MAX_RECIPIENTS
|
||||
|| message.purpose != purpose
|
||||
|| message.ciphertext.len() < message.encryption_type.minimum_ciphertext_len()
|
||||
|| message.recipients.iter().any(|recipient| {
|
||||
recipient.kem_ciphertext.len() != message.encryption_type.kem_ciphertext_len()
|
||||
|| recipient.encrypted_key.len() != message.encryption_type.wrapped_key_len()
|
||||
decrypt_multi_for_parts(
|
||||
message.encryption_type,
|
||||
message.purpose,
|
||||
&message.recipients,
|
||||
&message.ciphertext,
|
||||
purpose,
|
||||
keyring,
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypt an envelope represented by borrowed recipient and ciphertext
|
||||
/// slices. This keeps protected-value opening from cloning an already-owned
|
||||
/// envelope solely to call the cryptographic primitive.
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn decrypt_multi_for_parts(
|
||||
encryption_type: EncryptionType,
|
||||
envelope_purpose: u8,
|
||||
recipients: &[RecipientEntry],
|
||||
ciphertext: &[u8],
|
||||
purpose: u8,
|
||||
keyring: &Keyring,
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if recipients.is_empty()
|
||||
|| recipients.len() > MAX_RECIPIENTS
|
||||
|| envelope_purpose != purpose
|
||||
|| ciphertext.len() < encryption_type.minimum_ciphertext_len()
|
||||
|| recipients.iter().any(|recipient| {
|
||||
recipient.kem_ciphertext.len() != encryption_type.kem_ciphertext_len()
|
||||
|| recipient.encrypted_key.len() != encryption_type.wrapped_key_len()
|
||||
})
|
||||
{
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
|
||||
let payload_aad = payload_aad(message)?;
|
||||
for entry in &message.recipients {
|
||||
let count = u16::try_from(recipients.len()).map_err(|_| CryptoError::MalformedEnvelope)?;
|
||||
let mut payload_aad = Vec::new();
|
||||
payload_aad.extend_from_slice(ENCRYPT_DOMAIN);
|
||||
payload_aad.push(encryption_type.to_byte());
|
||||
payload_aad.push(envelope_purpose);
|
||||
payload_aad.extend_from_slice(&count.to_be_bytes());
|
||||
for entry in recipients {
|
||||
payload_aad.extend_from_slice(&entry.kem_ciphertext);
|
||||
payload_aad.extend_from_slice(&entry.encrypted_key);
|
||||
}
|
||||
for entry in recipients {
|
||||
let shared_secret =
|
||||
match HybridKem::decapsulate(&keyring.kem_secret_key, &entry.kem_ciphertext) {
|
||||
Ok(secret) => secret,
|
||||
|
|
@ -219,30 +315,51 @@ pub fn decrypt_multi_for(
|
|||
let wrap_key = Zeroizing::new(derive_encryption_key(
|
||||
&shared_secret,
|
||||
KEY_WRAP_DOMAIN,
|
||||
&[message.encryption_type.to_byte(), purpose],
|
||||
&[encryption_type.to_byte(), purpose],
|
||||
)?);
|
||||
let aad = wrap_aad(message.encryption_type, purpose, &entry.kem_ciphertext);
|
||||
let cek = match open_with_key(
|
||||
message.encryption_type,
|
||||
*wrap_key,
|
||||
&entry.encrypted_key,
|
||||
&aad,
|
||||
) {
|
||||
let aad = wrap_aad(encryption_type, purpose, &entry.kem_ciphertext);
|
||||
let cek = match open_with_key(encryption_type, *wrap_key, &entry.encrypted_key, &aad) {
|
||||
Ok(key) => key,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let cek: [u8; 32] = cek.try_into().map_err(|_| CryptoError::DecryptionFailed)?;
|
||||
return open_with_key(
|
||||
message.encryption_type,
|
||||
cek,
|
||||
&message.ciphertext,
|
||||
&payload_aad,
|
||||
);
|
||||
return open_with_key(encryption_type, cek, ciphertext, &payload_aad);
|
||||
}
|
||||
|
||||
Err(CryptoError::NoMatchingRecipient)
|
||||
}
|
||||
|
||||
/// Decrypt a canonical envelope only when its plaintext can fit inside the
|
||||
/// caller's allocation budget.
|
||||
///
|
||||
/// The AEAD implementation allocates its output buffer internally. Checking
|
||||
/// the ciphertext upper bound before entering that implementation makes the
|
||||
/// codec's reservation meaningful instead of merely checking the result
|
||||
/// after the allocation has already happened.
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn decrypt_multi_for_parts_with_limit(
|
||||
encryption_type: EncryptionType,
|
||||
envelope_purpose: u8,
|
||||
recipients: &[RecipientEntry],
|
||||
ciphertext: &[u8],
|
||||
purpose: u8,
|
||||
keyring: &Keyring,
|
||||
max_plaintext_len: usize,
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
if ciphertext.len() > max_plaintext_len {
|
||||
return Err(CryptoError::AllocationLimit);
|
||||
}
|
||||
|
||||
decrypt_multi_for_parts(
|
||||
encryption_type,
|
||||
envelope_purpose,
|
||||
recipients,
|
||||
ciphertext,
|
||||
purpose,
|
||||
keyring,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -325,4 +442,30 @@ mod tests {
|
|||
Err(CryptoError::MalformedEnvelope)
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn bounded_decryption_rejects_before_plaintext_allocation() -> Result<(), CryptoError> {
|
||||
let recipient = Keyring::generate();
|
||||
let message = encrypt_multi_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
1,
|
||||
b"bounded plaintext",
|
||||
&[recipient.public_key_bundle()],
|
||||
)?;
|
||||
|
||||
assert!(matches!(
|
||||
decrypt_multi_for_parts_with_limit(
|
||||
message.encryption_type,
|
||||
message.purpose,
|
||||
&message.recipients,
|
||||
&message.ciphertext,
|
||||
message.purpose,
|
||||
&recipient,
|
||||
message.ciphertext.len() - 1,
|
||||
),
|
||||
Err(CryptoError::AllocationLimit)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,3 +36,29 @@ pub fn derive_encryption_key(
|
|||
out.copy_from_slice(&key);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(feature = "password-kdf")]
|
||||
pub fn derive_password_key(
|
||||
passphrase: &[u8],
|
||||
salt: &[u8],
|
||||
memory_kib: u32,
|
||||
iterations: u32,
|
||||
lanes: u32,
|
||||
) -> Result<[u8; 32], CryptoError> {
|
||||
if passphrase.is_empty()
|
||||
|| salt.len() < 16
|
||||
|| !(8 * 1024..=256 * 1024).contains(&memory_kib)
|
||||
|| !(1..=10).contains(&iterations)
|
||||
|| !(1..=8).contains(&lanes)
|
||||
{
|
||||
return Err(CryptoError::KdfError);
|
||||
}
|
||||
let params = argon2::Params::new(memory_kib, iterations, lanes, Some(32))
|
||||
.map_err(|_| CryptoError::KdfError)?;
|
||||
let argon = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||
let mut key = [0u8; 32];
|
||||
argon
|
||||
.hash_password_into(passphrase, salt, &mut key)
|
||||
.map_err(|_| CryptoError::KdfError)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,9 +340,9 @@ impl Keyring {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
|
||||
#[deprecated(note = "use try_to_bytes for the primary fallible serializer")]
|
||||
pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>, crate::error::CryptoError> {
|
||||
self.try_to_bytes()
|
||||
.expect("key material length exceeds wire limit")
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::CryptoError> {
|
||||
|
|
@ -383,16 +383,26 @@ impl Keyring {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn to_hex(&self) -> String {
|
||||
bytes_to_hex(&self.to_bytes())
|
||||
#[deprecated(note = "use try_to_hex for the primary fallible serializer")]
|
||||
pub fn to_hex(&self) -> Result<String, crate::error::CryptoError> {
|
||||
self.try_to_hex()
|
||||
}
|
||||
|
||||
pub fn try_to_hex(&self) -> Result<String, crate::error::CryptoError> {
|
||||
Ok(bytes_to_hex(&self.try_to_bytes()?))
|
||||
}
|
||||
|
||||
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
|
||||
Self::from_bytes(&hex_to_bytes(s)?)
|
||||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
bytes_to_base64(&self.to_bytes())
|
||||
#[deprecated(note = "use try_to_base64 for the primary fallible serializer")]
|
||||
pub fn to_base64(&self) -> Result<String, crate::error::CryptoError> {
|
||||
self.try_to_base64()
|
||||
}
|
||||
|
||||
pub fn try_to_base64(&self) -> Result<String, crate::error::CryptoError> {
|
||||
Ok(bytes_to_base64(&self.try_to_bytes()?))
|
||||
}
|
||||
|
||||
pub fn from_base64(s: &str) -> Result<Self, crate::error::CryptoError> {
|
||||
|
|
@ -507,9 +517,9 @@ impl PublicKeyBundle {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
#[deprecated(note = "use try_as_bytes for the primary fallible serializer")]
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>, crate::error::CryptoError> {
|
||||
self.try_as_bytes()
|
||||
.expect("public key bundle field exceeds wire limit")
|
||||
}
|
||||
|
||||
/// Parse a complete suite-compatible public bundle.
|
||||
|
|
@ -582,8 +592,13 @@ impl PublicKeyBundle {
|
|||
Self::from_bytes(bytes)
|
||||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
bytes_to_base64(&self.as_bytes())
|
||||
#[deprecated(note = "use try_to_base64 for the primary fallible serializer")]
|
||||
pub fn to_base64(&self) -> Result<String, crate::error::CryptoError> {
|
||||
self.try_to_base64()
|
||||
}
|
||||
|
||||
pub fn try_to_base64(&self) -> Result<String, crate::error::CryptoError> {
|
||||
Ok(bytes_to_base64(&self.try_as_bytes()?))
|
||||
}
|
||||
|
||||
pub fn from_base64(s: &str) -> Result<Self, crate::error::CryptoError> {
|
||||
|
|
@ -602,12 +617,6 @@ impl TryFrom<&[u8]> for PublicKeyBundle {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&PublicKeyBundle> for Vec<u8> {
|
||||
fn from(bundle: &PublicKeyBundle) -> Vec<u8> {
|
||||
bundle.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PublicKeyBundle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PublicKeyBundle")
|
||||
|
|
@ -629,7 +638,7 @@ mod tests {
|
|||
let cl = SignaturePublicKey::new(vec![3u8; 32]);
|
||||
|
||||
let bundle = PublicKeyBundle::new(kem, pq, cl);
|
||||
let bytes = bundle.as_bytes();
|
||||
let bytes = bundle.try_as_bytes()?;
|
||||
let recovered = PublicKeyBundle::from_bytes_unvalidated(&bytes)?;
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -672,9 +681,9 @@ mod tests {
|
|||
SignaturePqPublicKey::new(vec![0xCDu8; 96]),
|
||||
SignaturePublicKey::new(vec![0xEFu8; 32]),
|
||||
);
|
||||
let bytes: Vec<u8> = Vec::from(&bundle);
|
||||
let bytes = bundle.try_as_bytes()?;
|
||||
let recovered = PublicKeyBundle::from_bytes_unvalidated(bytes.as_slice())?;
|
||||
assert_eq!(bundle.as_bytes(), recovered.as_bytes());
|
||||
assert_eq!(bundle.try_as_bytes()?, recovered.try_as_bytes()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -688,8 +697,8 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![5u8; 32]),
|
||||
SignaturePrivateKey::new(vec![6u8; 32]),
|
||||
);
|
||||
let bytes = keyring.to_bytes();
|
||||
let recovered = Keyring::from_bytes(&bytes)?;
|
||||
let bytes = keyring.try_to_bytes()?;
|
||||
let recovered = Keyring::from_bytes(bytes.as_slice())?;
|
||||
assert_eq!(
|
||||
keyring.kem_public_key.as_bytes(),
|
||||
recovered.kem_public_key.as_bytes()
|
||||
|
|
@ -722,7 +731,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_key_parsers_reject_trailing_bytes() {
|
||||
fn canonical_key_parsers_reject_trailing_bytes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let keyring = Keyring::new(
|
||||
KemPublicKey::new(vec![1u8; 16]),
|
||||
KemPrivateKey::new(vec![2u8; 16]),
|
||||
|
|
@ -731,25 +740,40 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![5u8; 16]),
|
||||
SignaturePrivateKey::new(vec![6u8; 16]),
|
||||
);
|
||||
let mut keyring_bytes = keyring.to_bytes().to_vec();
|
||||
let mut keyring_bytes = keyring.try_to_bytes()?.to_vec();
|
||||
keyring_bytes.push(0xAA);
|
||||
assert!(Keyring::from_bytes(&keyring_bytes).is_err());
|
||||
|
||||
let bundle = keyring.public_key_bundle();
|
||||
let mut bundle_bytes = bundle.as_bytes();
|
||||
let mut bundle_bytes = bundle.try_as_bytes()?;
|
||||
bundle_bytes.push(0xBB);
|
||||
assert!(PublicKeyBundle::from_bytes(&bundle_bytes).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_bundle_rejects_partial_suite_keys() {
|
||||
fn public_key_bundle_try_as_bytes_rejects_fields_larger_than_wire_length() {
|
||||
let bundle = PublicKeyBundle::new(
|
||||
KemPublicKey::new(vec![0u8; 65_536]),
|
||||
SignaturePqPublicKey::new(Vec::new()),
|
||||
SignaturePublicKey::new(Vec::new()),
|
||||
);
|
||||
assert!(matches!(
|
||||
bundle.try_as_bytes(),
|
||||
Err(crate::error::CryptoError::InvalidKeyLength)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_bundle_rejects_partial_suite_keys() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bundle = PublicKeyBundle::new(
|
||||
KemPublicKey::new(vec![1u8; 32]),
|
||||
SignaturePqPublicKey::new(vec![2u8; 64]),
|
||||
SignaturePublicKey::new(vec![3u8; 32]),
|
||||
);
|
||||
assert!(bundle.validate().is_err());
|
||||
assert!(PublicKeyBundle::from_bytes_validated(&bundle.as_bytes()).is_err());
|
||||
assert!(PublicKeyBundle::from_bytes_validated(&bundle.try_as_bytes()?).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -762,9 +786,9 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![4u8; 16]),
|
||||
SignaturePrivateKey::new(vec![5u8; 16]),
|
||||
);
|
||||
let bytes = keyring.to_bytes();
|
||||
let bytes = keyring.try_to_bytes()?;
|
||||
let recovered = Keyring::try_from(bytes.as_slice())?;
|
||||
assert_eq!(keyring.to_bytes(), recovered.to_bytes());
|
||||
assert_eq!(keyring.try_to_bytes()?, recovered.try_to_bytes()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -788,9 +812,9 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![5u8; 16]),
|
||||
SignaturePrivateKey::new(vec![6u8; 16]),
|
||||
);
|
||||
let hex = keyring.to_hex();
|
||||
let hex = keyring.try_to_hex()?;
|
||||
let recovered = Keyring::from_hex(&hex)?;
|
||||
assert_eq!(keyring.to_bytes(), recovered.to_bytes());
|
||||
assert_eq!(keyring.try_to_bytes()?, recovered.try_to_bytes()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -804,9 +828,9 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![5u8; 16]),
|
||||
SignaturePrivateKey::new(vec![6u8; 16]),
|
||||
);
|
||||
let b64 = keyring.to_base64();
|
||||
let b64 = keyring.try_to_base64()?;
|
||||
let recovered = Keyring::from_base64(&b64)?;
|
||||
assert_eq!(keyring.to_bytes(), recovered.to_bytes());
|
||||
assert_eq!(keyring.try_to_bytes()?, recovered.try_to_bytes()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -817,9 +841,9 @@ mod tests {
|
|||
SignaturePqPublicKey::new(vec![2u8; 64]),
|
||||
SignaturePublicKey::new(vec![3u8; 32]),
|
||||
);
|
||||
let b64 = bundle.to_base64();
|
||||
let b64 = bundle.try_to_base64()?;
|
||||
let recovered = PublicKeyBundle::from_base64_unvalidated(&b64)?;
|
||||
assert_eq!(bundle.as_bytes(), recovered.as_bytes());
|
||||
assert_eq!(bundle.try_as_bytes()?, recovered.try_as_bytes()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ pub use sign::{DualSignature, DualSigner, sign_dual};
|
|||
#[cfg(feature = "sha2")]
|
||||
pub use hash::{Sha256Hasher, sha256, sha256_double};
|
||||
|
||||
#[cfg(feature = "password-kdf")]
|
||||
pub use kdf::derive_password_key;
|
||||
#[cfg(feature = "hkdf")]
|
||||
pub use kdf::{derive_encryption_key, hkdf_expand, hkdf_extract};
|
||||
|
||||
|
|
@ -83,7 +85,9 @@ pub use helper::{ENCRYPT_DOMAIN, KEY_WRAP_DOMAIN};
|
|||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub use helper::{
|
||||
MAX_RECIPIENTS, MultiEncryptedMessage, RecipientEntry, decrypt_multi_for, encrypt_multi_for,
|
||||
MAX_RECIPIENTS, MultiEncryptedMessage, MultiEncryptedMessageRef, RecipientEntry,
|
||||
decrypt_multi_for, decrypt_multi_for_parts, decrypt_multi_for_parts_with_limit,
|
||||
encrypt_multi_for,
|
||||
};
|
||||
|
||||
/* ================================ TESTS ================================ */
|
||||
|
|
@ -311,7 +315,9 @@ mod tests {
|
|||
#[test]
|
||||
fn keyring_serialize_roundtrip() {
|
||||
let kr = Keyring::generate();
|
||||
let bytes = kr.to_bytes();
|
||||
let bytes = kr
|
||||
.try_to_bytes()
|
||||
.expect("keyring serialization should succeed");
|
||||
let loaded = Keyring::from_bytes(&bytes).expect("keyring roundtrip should succeed");
|
||||
assert_eq!(
|
||||
kr.kem_public_key.as_bytes(),
|
||||
|
|
@ -332,7 +338,9 @@ mod tests {
|
|||
fn public_key_bundle_serialize_roundtrip() {
|
||||
let kr = Keyring::generate();
|
||||
let bundle = kr.public_key_bundle();
|
||||
let bytes = bundle.as_bytes();
|
||||
let bytes = bundle
|
||||
.try_as_bytes()
|
||||
.expect("bundle serialization should succeed");
|
||||
let loaded = PublicKeyBundle::from_bytes(&bytes).expect("bundle roundtrip should succeed");
|
||||
assert_eq!(
|
||||
bundle.kem_public_key.as_bytes(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue