[WIP] Security work While on holiday
This commit is contained in:
parent
a81ac4efca
commit
7f0231e3f1
109 changed files with 19694 additions and 5210 deletions
|
|
@ -18,7 +18,7 @@ sha2 = { version = "0.11", optional = true }
|
|||
zeroize = { version = "1.9", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
base64 = "0.22"
|
||||
rand_core = { version = "0.10.1" }
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
rand = "0.10.2"
|
||||
getrandom = "0.4.3"
|
||||
mlkem-tls = { version = "0.2", optional = true }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ use zeroize::Zeroizing;
|
|||
#[cfg(any(feature = "chacha20poly1305", feature = "aes-gcm"))]
|
||||
use getrandom::fill;
|
||||
|
||||
/// Authentication-tag length shared by the supported AEAD constructions.
|
||||
pub const AUTH_TAG_LEN: usize = 16;
|
||||
|
||||
/// Nonce length stored at the front of an XChaCha20-Poly1305 output.
|
||||
pub const XCHACHA20POLY1305_NONCE_LEN: usize = 24;
|
||||
|
||||
/// Nonce length stored at the front of an AES-256-GCM output.
|
||||
pub const AES256GCM_NONCE_LEN: usize = 12;
|
||||
|
||||
pub trait AeadEncrypt {
|
||||
fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError>;
|
||||
}
|
||||
|
|
@ -27,12 +36,12 @@ fn prepend_nonce(nonce: &[u8], ciphertext: &mut Vec<u8>) -> Vec<u8> {
|
|||
}
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
pub struct ChaCha20Poly1305 {
|
||||
pub struct XChaCha20Poly1305 {
|
||||
key: Zeroizing<[u8; 32]>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
impl ChaCha20Poly1305 {
|
||||
impl XChaCha20Poly1305 {
|
||||
pub fn new(key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
key: Zeroizing::new(key),
|
||||
|
|
@ -41,7 +50,7 @@ impl ChaCha20Poly1305 {
|
|||
}
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
impl AeadEncrypt for ChaCha20Poly1305 {
|
||||
impl AeadEncrypt for XChaCha20Poly1305 {
|
||||
fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use chacha20poly1305::XNonce;
|
||||
|
|
@ -50,7 +59,7 @@ impl AeadEncrypt for ChaCha20Poly1305 {
|
|||
let key = chacha20poly1305::Key::from_slice(self.key.as_ref());
|
||||
let cipher = XChaCha20Poly1305::new(key);
|
||||
|
||||
let mut nonce = [0u8; 24];
|
||||
let mut nonce = [0u8; XCHACHA20POLY1305_NONCE_LEN];
|
||||
fill(&mut nonce).map_err(|_| CryptoError::EncryptionFailed)?;
|
||||
let nonce_ref = XNonce::from_slice(&nonce);
|
||||
|
||||
|
|
@ -68,17 +77,17 @@ impl AeadEncrypt for ChaCha20Poly1305 {
|
|||
}
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
impl AeadDecrypt for ChaCha20Poly1305 {
|
||||
impl AeadDecrypt for XChaCha20Poly1305 {
|
||||
fn decrypt(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use chacha20poly1305::XNonce;
|
||||
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
|
||||
|
||||
if ciphertext.len() < 24 {
|
||||
if ciphertext.len() < XCHACHA20POLY1305_NONCE_LEN + AUTH_TAG_LEN {
|
||||
return Err(CryptoError::InvalidNonceLength);
|
||||
}
|
||||
|
||||
let (nonce, ct) = ciphertext.split_at(24);
|
||||
let (nonce, ct) = ciphertext.split_at(XCHACHA20POLY1305_NONCE_LEN);
|
||||
let key = chacha20poly1305::Key::from_slice(self.key.as_ref());
|
||||
let cipher = XChaCha20Poly1305::new(key);
|
||||
let nonce_ref = XNonce::from_slice(nonce);
|
||||
|
|
@ -92,12 +101,17 @@ impl AeadDecrypt for ChaCha20Poly1305 {
|
|||
}
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
impl AeadCipher for ChaCha20Poly1305 {
|
||||
impl AeadCipher for XChaCha20Poly1305 {
|
||||
fn key_size() -> usize {
|
||||
32
|
||||
}
|
||||
}
|
||||
|
||||
/// Compatibility alias for the original public name. The implementation is
|
||||
/// XChaCha20-Poly1305, including its 24-byte nonce format.
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
pub type ChaCha20Poly1305 = XChaCha20Poly1305;
|
||||
|
||||
#[cfg(feature = "aes-gcm")]
|
||||
pub struct Aes256Gcm {
|
||||
key: Zeroizing<[u8; 32]>,
|
||||
|
|
@ -122,7 +136,7 @@ impl AeadEncrypt for Aes256Gcm {
|
|||
let key = aes_gcm::Key::<AesGcmInner>::from_slice(self.key.as_ref());
|
||||
let cipher = AesGcmInner::new(key);
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
let mut nonce = [0u8; AES256GCM_NONCE_LEN];
|
||||
fill(&mut nonce).map_err(|_| CryptoError::EncryptionFailed)?;
|
||||
let nonce_ref = Nonce::from_slice(&nonce);
|
||||
|
||||
|
|
@ -146,11 +160,11 @@ impl AeadDecrypt for Aes256Gcm {
|
|||
use aes_gcm::Nonce;
|
||||
use aes_gcm::aead::{Aead, KeyInit, Payload};
|
||||
|
||||
if ciphertext.len() < 12 {
|
||||
if ciphertext.len() < AES256GCM_NONCE_LEN + AUTH_TAG_LEN {
|
||||
return Err(CryptoError::InvalidNonceLength);
|
||||
}
|
||||
|
||||
let (nonce, ct) = ciphertext.split_at(12);
|
||||
let (nonce, ct) = ciphertext.split_at(AES256GCM_NONCE_LEN);
|
||||
let key = aes_gcm::Key::<AesGcmInner>::from_slice(self.key.as_ref());
|
||||
let cipher = AesGcmInner::new(key);
|
||||
let nonce_ref = Nonce::from_slice(nonce);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::error::CryptoError;
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::kdf::derive_encryption_key;
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
use crate::kem::HybridKem;
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::keypair::{Keyring, PublicKeyBundle};
|
||||
|
||||
/*
|
||||
* Algorithm selector for encrypted containers.
|
||||
* Algorithm selector for encrypted values.
|
||||
*
|
||||
* Mirrors `SigAlgorithm` for signatures: a single marking byte identifies the
|
||||
* key-encapsulation mechanism and the AEAD used to seal a container. The byte
|
||||
* is stored as the first byte of every encrypted blob so the decryptor can pick
|
||||
* is stored as the first byte of every encrypted envelope so the decryptor can pick
|
||||
* the matching algorithm (and the matching keypair from a `Keyring`) without
|
||||
* any out-of-band agreement.
|
||||
*
|
||||
|
|
@ -34,7 +30,7 @@ impl EncryptionType {
|
|||
pub const ML_KEM_CHACHA20POLY1305: u8 = 0x01;
|
||||
pub const ML_KEM_AES256_GCM: u8 = 0x02;
|
||||
|
||||
/// The marking byte written at the front of an encrypted blob.
|
||||
/// The marking byte written at the front of an encrypted envelope.
|
||||
pub const fn to_byte(self) -> u8 {
|
||||
match self {
|
||||
Self::MlKemChaCha20Poly1305 => Self::ML_KEM_CHACHA20POLY1305,
|
||||
|
|
@ -50,6 +46,50 @@ impl EncryptionType {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of the content-encryption key wrapped for each recipient.
|
||||
pub const CONTENT_ENCRYPTION_KEY_LEN: usize = 32;
|
||||
|
||||
/// The fixed-size ciphertext emitted by the KEM selected by this suite.
|
||||
pub const fn kem_ciphertext_len(self) -> usize {
|
||||
match self {
|
||||
Self::MlKemChaCha20Poly1305 | Self::MlKemAes256Gcm => {
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
{
|
||||
HybridKem::ciphertext_len()
|
||||
}
|
||||
#[cfg(not(feature = "mlkem-tls"))]
|
||||
{
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes the selected AEAD prepends/appends to an encrypted payload.
|
||||
pub const fn aead_overhead(self) -> usize {
|
||||
match self {
|
||||
Self::MlKemChaCha20Poly1305 => {
|
||||
crate::aead::XCHACHA20POLY1305_NONCE_LEN + crate::aead::AUTH_TAG_LEN
|
||||
}
|
||||
Self::MlKemAes256Gcm => crate::aead::AES256GCM_NONCE_LEN + crate::aead::AUTH_TAG_LEN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Total output length for an encrypted plaintext of `plaintext_len` bytes.
|
||||
pub const fn encrypted_len(self, plaintext_len: usize) -> usize {
|
||||
plaintext_len.saturating_add(self.aead_overhead())
|
||||
}
|
||||
|
||||
/// Minimum valid AEAD output length for this suite.
|
||||
pub const fn minimum_ciphertext_len(self) -> usize {
|
||||
self.encrypted_len(0)
|
||||
}
|
||||
|
||||
/// The size of a wrapped 32-byte content key for this suite.
|
||||
pub const fn wrapped_key_len(self) -> usize {
|
||||
self.encrypted_len(Self::CONTENT_ENCRYPTION_KEY_LEN)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -59,7 +99,7 @@ impl EncryptionType {
|
|||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
#[allow(unused_variables)]
|
||||
fn aead_seal(
|
||||
pub fn seal_with_key(
|
||||
enc_type: EncryptionType,
|
||||
key: [u8; 32],
|
||||
plaintext: &[u8],
|
||||
|
|
@ -70,7 +110,7 @@ fn aead_seal(
|
|||
match enc_type {
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
EncryptionType::MlKemChaCha20Poly1305 => {
|
||||
crate::aead::ChaCha20Poly1305::new(key).encrypt(plaintext, aad)
|
||||
crate::aead::XChaCha20Poly1305::new(key).encrypt(plaintext, aad)
|
||||
}
|
||||
#[cfg(feature = "aes-gcm")]
|
||||
EncryptionType::MlKemAes256Gcm => crate::aead::Aes256Gcm::new(key).encrypt(plaintext, aad),
|
||||
|
|
@ -86,7 +126,7 @@ fn aead_seal(
|
|||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
#[allow(unused_variables)]
|
||||
fn aead_open(
|
||||
pub fn open_with_key(
|
||||
enc_type: EncryptionType,
|
||||
key: [u8; 32],
|
||||
ciphertext: &[u8],
|
||||
|
|
@ -97,7 +137,7 @@ fn aead_open(
|
|||
match enc_type {
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
EncryptionType::MlKemChaCha20Poly1305 => {
|
||||
crate::aead::ChaCha20Poly1305::new(key).decrypt(ciphertext, aad)
|
||||
crate::aead::XChaCha20Poly1305::new(key).decrypt(ciphertext, aad)
|
||||
}
|
||||
#[cfg(feature = "aes-gcm")]
|
||||
EncryptionType::MlKemAes256Gcm => crate::aead::Aes256Gcm::new(key).decrypt(ciphertext, aad),
|
||||
|
|
@ -106,76 +146,51 @@ fn aead_open(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
const ENC_KDF_SALT: &[u8] = b"mtp-container-enc";
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
const ENC_KDF_CONTEXT: &[u8] = b"single-recipient";
|
||||
|
||||
/*
|
||||
* Encrypt `plaintext` for a single recipient, selecting the algorithm with
|
||||
* `enc_type` and the recipient's KEM public key from `recipient`.
|
||||
*
|
||||
* The returned, self-describing blob is laid out as:
|
||||
* [1 byte EncryptionType] [2 bytes u16 kem_ct_len] [kem_ciphertext] [aead_payload]
|
||||
* where `aead_payload` is the AEAD output (nonce + ciphertext + tag). The AEAD
|
||||
* key is derived from the KEM shared secret via HKDF, so no separate content key
|
||||
* is transmitted.
|
||||
*
|
||||
* Requires the `mlkem-tls` and `hkdf` features, plus the AEAD feature backing
|
||||
* `enc_type`.
|
||||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn encrypt_for(
|
||||
enc_type: EncryptionType,
|
||||
recipient: &PublicKeyBundle,
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
let enc = HybridKem::encapsulate(&recipient.kem_public_key)?;
|
||||
let key = derive_encryption_key(&enc.shared_secret, ENC_KDF_SALT, ENC_KDF_CONTEXT)?;
|
||||
let aead_payload = aead_seal(enc_type, key, plaintext, aad)?;
|
||||
|
||||
let kem_ct = enc.ciphertext;
|
||||
let mut out = Vec::with_capacity(1 + 2 + kem_ct.len() + aead_payload.len());
|
||||
out.push(enc_type.to_byte());
|
||||
out.extend_from_slice(&(kem_ct.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&kem_ct);
|
||||
out.extend_from_slice(&aead_payload);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/*
|
||||
* Decrypt a blob produced by [`encrypt_for`] using `keyring`.
|
||||
*
|
||||
* The leading byte selects the `EncryptionType` (and thus which keypair to use
|
||||
* from the keyring); for the current ML-KEM variants that is `kem_secret_key`.
|
||||
* Returns `DecryptionFailed` on any malformed input or authentication failure.
|
||||
*
|
||||
* Requires the `mlkem-tls` and `hkdf` features, plus the AEAD feature backing
|
||||
* the blob's algorithm.
|
||||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn decrypt_with(blob: &[u8], keyring: &Keyring, aad: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
if blob.len() < 3 {
|
||||
return Err(CryptoError::DecryptionFailed);
|
||||
}
|
||||
let enc_type = EncryptionType::from_byte(blob[0]).ok_or(CryptoError::DecryptionFailed)?;
|
||||
let kem_ct_len = u16::from_be_bytes([blob[1], blob[2]]) as usize;
|
||||
let kem_end = 3usize
|
||||
.checked_add(kem_ct_len)
|
||||
.ok_or(CryptoError::DecryptionFailed)?;
|
||||
let kem_ct = blob.get(3..kem_end).ok_or(CryptoError::DecryptionFailed)?;
|
||||
let aead_payload = blob.get(kem_end..).ok_or(CryptoError::DecryptionFailed)?;
|
||||
|
||||
let shared_secret = HybridKem::decapsulate(&keyring.kem_secret_key, kem_ct)?;
|
||||
let key = derive_encryption_key(&shared_secret, ENC_KDF_SALT, ENC_KDF_CONTEXT)?;
|
||||
aead_open(enc_type, key, aead_payload, aad)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
#[test]
|
||||
fn envelope_parser_uses_suite_dependent_fixed_widths() {
|
||||
use crate::helper::{MultiEncryptedMessage, RecipientEntry};
|
||||
|
||||
let suites = [
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
EncryptionType::MlKemAes256Gcm,
|
||||
];
|
||||
assert_ne!(suites[0].wrapped_key_len(), suites[1].wrapped_key_len());
|
||||
|
||||
for (index, suite) in suites.into_iter().enumerate() {
|
||||
let marker = u8::try_from(index).unwrap();
|
||||
let message = MultiEncryptedMessage {
|
||||
encryption_type: suite,
|
||||
purpose: 0xA5,
|
||||
recipients: vec![RecipientEntry {
|
||||
kem_ciphertext: vec![0x10 + marker; suite.kem_ciphertext_len()],
|
||||
encrypted_key: vec![0x20 + marker; suite.wrapped_key_len()],
|
||||
}],
|
||||
ciphertext: vec![0x30 + marker; suite.minimum_ciphertext_len() + 3],
|
||||
};
|
||||
let encoded = message.to_bytes().expect("synthetic envelope is valid");
|
||||
let kem_end = 4 + suite.kem_ciphertext_len();
|
||||
let wrapped_end = kem_end + suite.wrapped_key_len();
|
||||
|
||||
assert_eq!(&encoded[..4], &[suite.to_byte(), 0xA5, 0, 1]);
|
||||
assert_eq!(&encoded[4..kem_end], message.recipients[0].kem_ciphertext);
|
||||
assert_eq!(
|
||||
&encoded[kem_end..wrapped_end],
|
||||
message.recipients[0].encrypted_key
|
||||
);
|
||||
assert_eq!(&encoded[wrapped_end..], message.ciphertext);
|
||||
assert_eq!(
|
||||
MultiEncryptedMessage::from_bytes(&encoded)
|
||||
.expect("suite-specific envelope should parse"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption_type_byte_roundtrip() {
|
||||
for t in [
|
||||
|
|
@ -188,59 +203,19 @@ mod tests {
|
|||
assert_eq!(EncryptionType::from_byte(0xFF), None);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn encrypt_for_roundtrip() -> Result<(), CryptoError> {
|
||||
let kr = Keyring::generate();
|
||||
let blob = encrypt_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
&kr.public_key_bundle(),
|
||||
b"secret payload",
|
||||
b"aad",
|
||||
)?;
|
||||
assert_eq!(blob[0], EncryptionType::ML_KEM_CHACHA20POLY1305);
|
||||
|
||||
let pt = decrypt_with(&blob, &kr, b"aad")?;
|
||||
assert_eq!(pt, b"secret payload");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn decrypt_with_wrong_keyring_fails() -> Result<(), CryptoError> {
|
||||
let kr = Keyring::generate();
|
||||
let other = Keyring::generate();
|
||||
let blob = encrypt_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
&kr.public_key_bundle(),
|
||||
b"secret",
|
||||
b"aad",
|
||||
)?;
|
||||
assert!(decrypt_with(&blob, &other, b"aad").is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn decrypt_with_wrong_aad_fails() -> Result<(), CryptoError> {
|
||||
let kr = Keyring::generate();
|
||||
let blob = encrypt_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
&kr.public_key_bundle(),
|
||||
b"secret",
|
||||
b"right",
|
||||
)?;
|
||||
assert!(decrypt_with(&blob, &kr, b"wrong").is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn decrypt_with_malformed_fails() {
|
||||
let kr = Keyring::generate();
|
||||
assert!(decrypt_with(b"", &kr, b"").is_err());
|
||||
assert!(decrypt_with(&[0x01, 0x00], &kr, b"").is_err());
|
||||
// Unknown algorithm byte.
|
||||
assert!(decrypt_with(&[0x7F, 0x00, 0x00], &kr, b"").is_err());
|
||||
fn suite_lengths_are_derived_from_the_selected_primitives() {
|
||||
assert_eq!(
|
||||
EncryptionType::MlKemChaCha20Poly1305.wrapped_key_len(),
|
||||
EncryptionType::CONTENT_ENCRYPTION_KEY_LEN
|
||||
+ crate::aead::XCHACHA20POLY1305_NONCE_LEN
|
||||
+ crate::aead::AUTH_TAG_LEN
|
||||
);
|
||||
assert_eq!(
|
||||
EncryptionType::MlKemAes256Gcm.wrapped_key_len(),
|
||||
EncryptionType::CONTENT_ENCRYPTION_KEY_LEN
|
||||
+ crate::aead::AES256GCM_NONCE_LEN
|
||||
+ crate::aead::AUTH_TAG_LEN
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,16 @@ pub enum CryptoError {
|
|||
EncryptionFailed,
|
||||
#[error("decryption failed")]
|
||||
DecryptionFailed,
|
||||
#[error("malformed encryption envelope")]
|
||||
MalformedEnvelope,
|
||||
#[error("no encryption recipients")]
|
||||
NoRecipients,
|
||||
#[error("no matching encryption recipient")]
|
||||
NoMatchingRecipient,
|
||||
#[error("invalid key length")]
|
||||
InvalidKeyLength,
|
||||
#[error("public and private key material do not match")]
|
||||
InvalidKeyMaterial,
|
||||
#[error("invalid nonce length")]
|
||||
InvalidNonceLength,
|
||||
#[error("invalid signature")]
|
||||
|
|
|
|||
|
|
@ -1,208 +1,328 @@
|
|||
// Canonical multi-recipient encryption envelopes.
|
||||
|
||||
use crate::enc::EncryptionType;
|
||||
use crate::error::CryptoError;
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
use crate::aead::{AeadDecrypt, AeadEncrypt, ChaCha20Poly1305};
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::enc::{open_with_key, seal_with_key};
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::kdf::derive_encryption_key;
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::kem::HybridKem;
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use crate::keypair::{Keyring, PublicKeyBundle};
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use rand::Rng;
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub const ENCRYPT_DOMAIN: &[u8] = b"MTP-DATA-ENC-1";
|
||||
pub const KEY_WRAP_DOMAIN: &[u8] = b"MTP-DATA-WRAP-1";
|
||||
/// Operational cap for recipient entries accepted in one envelope.
|
||||
///
|
||||
/// The wire count remains a `u16` for format stability, but decapsulation is
|
||||
/// intentionally bounded because each entry can require a KEM operation.
|
||||
pub const MAX_RECIPIENTS: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RecipientEntry {
|
||||
pub kem_ciphertext: Vec<u8>,
|
||||
pub encrypted_key: Vec<u8>,
|
||||
}
|
||||
|
||||
/*
|
||||
* A payload encrypted for multiple recipients.
|
||||
*
|
||||
* Any recipient who possesses the corresponding `KemPrivateKey` can decrypt the message.
|
||||
*/
|
||||
/// The envelope body used by `DataValue::Encrypted`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MultiEncryptedMessage {
|
||||
pub encryption_type: EncryptionType,
|
||||
pub purpose: u8,
|
||||
pub recipients: Vec<RecipientEntry>,
|
||||
pub nonce: [u8; 24],
|
||||
/// The AEAD output, including its nonce as defined by the selected suite.
|
||||
pub ciphertext: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MultiEncryptedMessage {
|
||||
/*
|
||||
* Serialize into a compact byte vector.
|
||||
*
|
||||
* Format:
|
||||
* - `num_recipients: u16`
|
||||
* - for each recipient:
|
||||
* - `kem_ct_len: u16` | `kem_ciphertext`
|
||||
* - `ek_len: u16` | `encrypted_key`
|
||||
* - `nonce: 24 bytes`
|
||||
* - `ciphertext` (remaining)
|
||||
*/
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
/// Serialize the envelope body without redundant per-recipient lengths.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, CryptoError> {
|
||||
let kem_len = self.encryption_type.kem_ciphertext_len();
|
||||
let wrapped_len = self.encryption_type.wrapped_key_len();
|
||||
let count =
|
||||
u16::try_from(self.recipients.len()).map_err(|_| CryptoError::EncryptionFailed)?;
|
||||
if self.recipients.is_empty()
|
||||
|| self.recipients.len() > MAX_RECIPIENTS
|
||||
|| self.ciphertext.len() < self.encryption_type.minimum_ciphertext_len()
|
||||
|| self
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|r| r.kem_ciphertext.len() != kem_len || r.encrypted_key.len() != wrapped_len)
|
||||
{
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&(self.recipients.len() as u16).to_be_bytes());
|
||||
for r in &self.recipients {
|
||||
out.extend_from_slice(&(r.kem_ciphertext.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&r.kem_ciphertext);
|
||||
out.extend_from_slice(&(r.encrypted_key.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&r.encrypted_key);
|
||||
out.push(self.encryption_type.to_byte());
|
||||
out.push(self.purpose);
|
||||
out.extend_from_slice(&count.to_be_bytes());
|
||||
for recipient in &self.recipients {
|
||||
out.extend_from_slice(&recipient.kem_ciphertext);
|
||||
out.extend_from_slice(&recipient.encrypted_key);
|
||||
}
|
||||
out.extend_from_slice(&self.nonce);
|
||||
out.extend_from_slice(&self.ciphertext);
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Deserialize from bytes produced by `to_bytes`.
|
||||
/// Parse the canonical envelope body.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
|
||||
let mut offset = 0;
|
||||
let read_u16 = |off: &mut usize| -> Result<u16, CryptoError> {
|
||||
let slice = bytes
|
||||
.get(*off..*off + 2)
|
||||
.ok_or(CryptoError::DecryptionFailed)?;
|
||||
let arr: [u8; 2] = slice
|
||||
.try_into()
|
||||
.map_err(|_| CryptoError::DecryptionFailed)?;
|
||||
*off += 2;
|
||||
Ok(u16::from_be_bytes(arr))
|
||||
};
|
||||
|
||||
let num = read_u16(&mut offset)? as usize;
|
||||
let mut recipients = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
let klen = read_u16(&mut offset)? as usize;
|
||||
let kem_ct = bytes
|
||||
.get(offset..offset + klen)
|
||||
.ok_or(CryptoError::DecryptionFailed)?
|
||||
.to_vec();
|
||||
offset += klen;
|
||||
|
||||
let elen = read_u16(&mut offset)? as usize;
|
||||
let enc_key = bytes
|
||||
.get(offset..offset + elen)
|
||||
.ok_or(CryptoError::DecryptionFailed)?
|
||||
.to_vec();
|
||||
offset += elen;
|
||||
|
||||
recipients.push(RecipientEntry {
|
||||
kem_ciphertext: kem_ct,
|
||||
encrypted_key: enc_key,
|
||||
});
|
||||
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 nonce: [u8; 24] = bytes
|
||||
.get(offset..offset + 24)
|
||||
.ok_or(CryptoError::DecryptionFailed)?
|
||||
.try_into()
|
||||
.map_err(|_| CryptoError::DecryptionFailed)?;
|
||||
offset += 24;
|
||||
|
||||
let ciphertext = bytes
|
||||
.get(offset..)
|
||||
.ok_or(CryptoError::DecryptionFailed)?
|
||||
.to_vec();
|
||||
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,
|
||||
nonce,
|
||||
ciphertext,
|
||||
ciphertext: bytes[offset..].to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Encrypt `plaintext` for every recipient in `entities`.
|
||||
*
|
||||
* Internally generates a fresh content-encryption key, encrypts the payload
|
||||
* with ChaCha20-Poly1305, then KEM-encapsulates and wraps the key for each
|
||||
* recipient. The returned `MultiEncryptedMessage` can be decrypted by any
|
||||
* entity whose keyring contains the corresponding private KEM key.
|
||||
*
|
||||
* Requires the `pqc` and `chacha20poly1305` features.
|
||||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
pub fn encrypt_multi(
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
fn wrap_aad(encryption_type: EncryptionType, purpose: u8, kem_ciphertext: &[u8]) -> Vec<u8> {
|
||||
let mut aad = Vec::with_capacity(KEY_WRAP_DOMAIN.len() + 2 + kem_ciphertext.len());
|
||||
aad.extend_from_slice(KEY_WRAP_DOMAIN);
|
||||
aad.push(encryption_type.to_byte());
|
||||
aad.push(purpose);
|
||||
aad.extend_from_slice(kem_ciphertext);
|
||||
aad
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
fn payload_aad(message: &MultiEncryptedMessage) -> Result<Vec<u8>, CryptoError> {
|
||||
let count =
|
||||
u16::try_from(message.recipients.len()).map_err(|_| CryptoError::MalformedEnvelope)?;
|
||||
let mut aad = Vec::new();
|
||||
aad.extend_from_slice(ENCRYPT_DOMAIN);
|
||||
aad.push(message.encryption_type.to_byte());
|
||||
aad.push(message.purpose);
|
||||
aad.extend_from_slice(&count.to_be_bytes());
|
||||
for recipient in &message.recipients {
|
||||
aad.extend_from_slice(&recipient.kem_ciphertext);
|
||||
aad.extend_from_slice(&recipient.encrypted_key);
|
||||
}
|
||||
Ok(aad)
|
||||
}
|
||||
|
||||
/// Encrypt a value for one or more recipients using the canonical envelope.
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn encrypt_multi_for(
|
||||
encryption_type: EncryptionType,
|
||||
purpose: u8,
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
entities: &[PublicKeyBundle],
|
||||
) -> Result<MultiEncryptedMessage, CryptoError> {
|
||||
if entities.is_empty() {
|
||||
return Err(CryptoError::NoRecipients);
|
||||
}
|
||||
if entities.len() > MAX_RECIPIENTS {
|
||||
return Err(CryptoError::EncryptionFailed);
|
||||
}
|
||||
|
||||
let mut cek = Zeroizing::new([0u8; 32]);
|
||||
rand::rng().fill_bytes(cek.as_mut());
|
||||
|
||||
let cipher = ChaCha20Poly1305::new(*cek);
|
||||
let encrypted_payload = cipher.encrypt(plaintext, aad)?;
|
||||
|
||||
let nonce: [u8; 24] = encrypted_payload[..24]
|
||||
.try_into()
|
||||
.map_err(|_| CryptoError::EncryptionFailed)?;
|
||||
let ciphertext = encrypted_payload[24..].to_vec();
|
||||
|
||||
let mut recipients = Vec::with_capacity(entities.len());
|
||||
for entity in entities {
|
||||
let enc = HybridKem::encapsulate(&entity.kem_public_key)?;
|
||||
let wrap_key = Zeroizing::new(derive_encryption_key(
|
||||
&enc.shared_secret,
|
||||
b"mtp-multi-key-wrap",
|
||||
b"multi-recipient",
|
||||
KEY_WRAP_DOMAIN,
|
||||
&[encryption_type.to_byte(), purpose],
|
||||
)?);
|
||||
|
||||
let wrap_cipher = ChaCha20Poly1305::new(*wrap_key);
|
||||
let encrypted_key = wrap_cipher.encrypt(cek.as_ref(), b"")?;
|
||||
|
||||
let aad = wrap_aad(encryption_type, purpose, &enc.ciphertext);
|
||||
let encrypted_key = seal_with_key(encryption_type, *wrap_key, cek.as_ref(), &aad)?;
|
||||
recipients.push(RecipientEntry {
|
||||
kem_ciphertext: enc.ciphertext,
|
||||
encrypted_key,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(MultiEncryptedMessage {
|
||||
let mut message = MultiEncryptedMessage {
|
||||
encryption_type,
|
||||
purpose,
|
||||
recipients,
|
||||
nonce,
|
||||
ciphertext,
|
||||
})
|
||||
ciphertext: Vec::new(),
|
||||
};
|
||||
let aad = payload_aad(&message)?;
|
||||
message.ciphertext = seal_with_key(encryption_type, *cek, plaintext, &aad)?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/*
|
||||
* Decrypt a `MultiEncryptedMessage` using the recipient's `Keyring`.
|
||||
*
|
||||
* Tries each `RecipientEntry` until one succeeds with the given keyring's
|
||||
* KEM secret key. Returns the original plaintext.
|
||||
*/
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
pub fn decrypt_multi(
|
||||
msg: &MultiEncryptedMessage,
|
||||
aad: &[u8],
|
||||
/// Decrypt a canonical envelope for a recipient in `keyring`.
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub fn decrypt_multi_for(
|
||||
message: &MultiEncryptedMessage,
|
||||
purpose: u8,
|
||||
keyring: &Keyring,
|
||||
) -> Result<Vec<u8>, CryptoError> {
|
||||
for entry in &msg.recipients {
|
||||
let ss = match HybridKem::decapsulate(&keyring.kem_secret_key, &entry.kem_ciphertext) {
|
||||
Ok(s) => s,
|
||||
Err(_) => continue,
|
||||
};
|
||||
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()
|
||||
})
|
||||
{
|
||||
return Err(CryptoError::MalformedEnvelope);
|
||||
}
|
||||
|
||||
let payload_aad = payload_aad(message)?;
|
||||
for entry in &message.recipients {
|
||||
let shared_secret =
|
||||
match HybridKem::decapsulate(&keyring.kem_secret_key, &entry.kem_ciphertext) {
|
||||
Ok(secret) => secret,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let wrap_key = Zeroizing::new(derive_encryption_key(
|
||||
&ss,
|
||||
b"mtp-multi-key-wrap",
|
||||
b"multi-recipient",
|
||||
&shared_secret,
|
||||
KEY_WRAP_DOMAIN,
|
||||
&[message.encryption_type.to_byte(), purpose],
|
||||
)?);
|
||||
let wrap_cipher = ChaCha20Poly1305::new(*wrap_key);
|
||||
let cek = match wrap_cipher.decrypt(&entry.encrypted_key, b"") {
|
||||
Ok(k) => Zeroizing::new(k),
|
||||
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,
|
||||
) {
|
||||
Ok(key) => key,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let cek_arr = Zeroizing::new(
|
||||
cek.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| CryptoError::DecryptionFailed)?,
|
||||
let cek: [u8; 32] = cek.try_into().map_err(|_| CryptoError::DecryptionFailed)?;
|
||||
return open_with_key(
|
||||
message.encryption_type,
|
||||
cek,
|
||||
&message.ciphertext,
|
||||
&payload_aad,
|
||||
);
|
||||
}
|
||||
|
||||
Err(CryptoError::NoMatchingRecipient)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn recipient_count_is_operationally_bounded() {
|
||||
assert!(matches!(
|
||||
MultiEncryptedMessage::from_bytes(&[EncryptionType::ML_KEM_CHACHA20POLY1305, 0, 0, 65]),
|
||||
Err(CryptoError::MalformedEnvelope)
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf", feature = "chacha20poly1305"))]
|
||||
#[test]
|
||||
fn authenticated_envelope_fields_reject_tampering() -> Result<(), CryptoError> {
|
||||
let recipient_a = Keyring::generate();
|
||||
let recipient_b = Keyring::generate();
|
||||
let message = encrypt_multi_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
7,
|
||||
b"authenticated payload",
|
||||
&[
|
||||
recipient_a.public_key_bundle(),
|
||||
recipient_b.public_key_bundle(),
|
||||
],
|
||||
)?;
|
||||
assert_eq!(
|
||||
decrypt_multi_for(&message, message.purpose, &recipient_a)?,
|
||||
b"authenticated payload"
|
||||
);
|
||||
|
||||
let mut full_ct = Vec::with_capacity(24 + msg.ciphertext.len());
|
||||
full_ct.extend_from_slice(&msg.nonce);
|
||||
full_ct.extend_from_slice(&msg.ciphertext);
|
||||
let mut wrong_purpose = message.clone();
|
||||
wrong_purpose.purpose ^= 1;
|
||||
assert!(
|
||||
decrypt_multi_for(&wrong_purpose, wrong_purpose.purpose, &recipient_a).is_err(),
|
||||
"mutating the encryption purpose must invalidate the envelope"
|
||||
);
|
||||
|
||||
let data_cipher = ChaCha20Poly1305::new(*cek_arr);
|
||||
return data_cipher.decrypt(&full_ct, aad);
|
||||
let mut wrong_recipient_table = message.clone();
|
||||
wrong_recipient_table.recipients[1].encrypted_key[0] ^= 1;
|
||||
assert!(
|
||||
decrypt_multi_for(&wrong_recipient_table, message.purpose, &recipient_a).is_err(),
|
||||
"mutating another recipient's table entry must invalidate the payload"
|
||||
);
|
||||
|
||||
let mut wrong_ciphertext = message;
|
||||
let last = wrong_ciphertext.ciphertext.len() - 1;
|
||||
wrong_ciphertext.ciphertext[last] ^= 1;
|
||||
assert!(
|
||||
decrypt_multi_for(&wrong_ciphertext, wrong_ciphertext.purpose, &recipient_a).is_err(),
|
||||
"mutating the ciphertext must invalidate the envelope"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
#[test]
|
||||
fn rejects_envelopes_without_a_complete_aead_payload() {
|
||||
let encryption_type = EncryptionType::MlKemChaCha20Poly1305;
|
||||
let message = MultiEncryptedMessage {
|
||||
encryption_type,
|
||||
purpose: 1,
|
||||
recipients: vec![RecipientEntry {
|
||||
kem_ciphertext: vec![0; encryption_type.kem_ciphertext_len()],
|
||||
encrypted_key: vec![0; encryption_type.wrapped_key_len()],
|
||||
}],
|
||||
ciphertext: vec![0; encryption_type.minimum_ciphertext_len() - 1],
|
||||
};
|
||||
assert!(matches!(
|
||||
message.to_bytes(),
|
||||
Err(CryptoError::MalformedEnvelope)
|
||||
));
|
||||
|
||||
let mut encoded = vec![encryption_type.to_byte(), 1, 0, 1];
|
||||
encoded.extend_from_slice(&vec![0; encryption_type.kem_ciphertext_len()]);
|
||||
encoded.extend_from_slice(&vec![0; encryption_type.wrapped_key_len()]);
|
||||
encoded.extend_from_slice(&vec![0; encryption_type.minimum_ciphertext_len() - 1]);
|
||||
assert!(matches!(
|
||||
MultiEncryptedMessage::from_bytes(&encoded),
|
||||
Err(CryptoError::MalformedEnvelope)
|
||||
));
|
||||
}
|
||||
Err(CryptoError::DecryptionFailed)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ pub struct HybridKem;
|
|||
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
impl HybridKem {
|
||||
/// Fixed wire size of the KEM ciphertext used by MTP envelopes.
|
||||
pub const fn ciphertext_len() -> usize {
|
||||
mlkem_tls::X25519MlKem768::CIPHERTEXT_SIZE
|
||||
}
|
||||
|
||||
pub fn generate_keypair() -> (KemPrivateKey, KemPublicKey) {
|
||||
/* Obviously: cannot find module or crate rand_core06 in this scope
|
||||
use of unresolved module or unlinked crate rand_core06 (rustc E0433) */
|
||||
let (ek, dk) =
|
||||
mlkem_tls::X25519MlKem768::keygen(&mut chacha20poly1305::aead::rand_core::OsRng);
|
||||
let (ek, dk) = mlkem_tls::X25519MlKem768::keygen(&mut rand_core::OsRng);
|
||||
(
|
||||
KemPrivateKey::new(dk.as_bytes().to_vec()),
|
||||
KemPublicKey::new(ek.as_bytes().to_vec()),
|
||||
|
|
@ -26,10 +28,7 @@ impl HybridKem {
|
|||
pub fn encapsulate(recipient_pk: &KemPublicKey) -> Result<Encapsulated, CryptoError> {
|
||||
let ek = mlkem_tls::EncapsKey768::try_from(recipient_pk.as_bytes())
|
||||
.map_err(|_| CryptoError::KemEncapsulationFailed)?;
|
||||
let (ct, ss) = mlkem_tls::X25519MlKem768::encapsulate(
|
||||
&ek,
|
||||
&mut chacha20poly1305::aead::rand_core::OsRng,
|
||||
);
|
||||
let (ct, ss) = mlkem_tls::X25519MlKem768::encapsulate(&ek, &mut rand_core::OsRng);
|
||||
Ok(Encapsulated {
|
||||
ciphertext: ct.as_bytes().to_vec(),
|
||||
shared_secret: Zeroizing::new(ss.as_bytes().to_vec()),
|
||||
|
|
|
|||
|
|
@ -237,7 +237,91 @@ impl Keyring {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
|
||||
/// Validate the material required to produce classical signatures. This
|
||||
/// intentionally permits a browser role-specific keyring without KEM or
|
||||
/// PQ fields.
|
||||
pub fn validate_ed25519_signing(&self) -> Result<(), crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
if self.sig_cl_secret_key.as_bytes().len() != 32
|
||||
|| self.sig_cl_public_key.as_bytes().len() != SIG_CL_PUBLIC_KEY_LEN
|
||||
{
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
#[cfg(feature = "ed25519-dalek")]
|
||||
{
|
||||
let signer = crate::sign::Ed25519Signer::new(&self.sig_cl_secret_key)?;
|
||||
if signer.public_key().as_bytes() != self.sig_cl_public_key.as_bytes() {
|
||||
return Err(CryptoError::InvalidKeyMaterial);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate material required for a hybrid Ed25519 + ML-DSA signature.
|
||||
pub fn validate_dual_signing(&self) -> Result<(), crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
self.validate_ed25519_signing()?;
|
||||
if self.sig_pq_secret_key.as_bytes().len() != 32
|
||||
|| self.sig_pq_public_key.as_bytes().len() != SIG_PQ_PUBLIC_KEY_LEN
|
||||
{
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
#[cfg(feature = "ml-dsa")]
|
||||
{
|
||||
let signer =
|
||||
crate::sign::MlDsaSigner::new(&self.sig_pq_secret_key, &self.sig_pq_public_key)?;
|
||||
if signer.public_key().as_bytes() != self.sig_pq_public_key.as_bytes() {
|
||||
return Err(CryptoError::InvalidKeyMaterial);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate the KEM material required to decrypt envelopes addressed to
|
||||
/// this keyring. This is intentionally separate from full identity
|
||||
/// validation because browser and relay roles may use Ed25519-only
|
||||
/// signing material while still needing a complete encryption key pair.
|
||||
pub fn validate_encryption(&self) -> Result<(), crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
if self.kem_public_key.as_bytes().is_empty() || self.kem_secret_key.as_bytes().is_empty() {
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
{
|
||||
let encapsulated = crate::kem::HybridKem::encapsulate(&self.kem_public_key)?;
|
||||
let recovered =
|
||||
crate::kem::HybridKem::decapsulate(&self.kem_secret_key, &encapsulated.ciphertext)?;
|
||||
if recovered.as_slice() != encapsulated.shared_secret.as_slice() {
|
||||
return Err(CryptoError::InvalidKeyMaterial);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a complete identity before using it at a protocol boundary.
|
||||
///
|
||||
/// `Keyring` remains permissive because browser callers may intentionally
|
||||
/// hold role-specific material. Protocol paths that need encryption and
|
||||
/// both signing suites should call this method explicitly.
|
||||
pub fn validate_full(&self) -> Result<(), crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
|
||||
self.public_key_bundle().validate()?;
|
||||
self.validate_encryption()?;
|
||||
if self.sig_pq_secret_key.as_bytes().len() != 32
|
||||
|| self.sig_cl_secret_key.as_bytes().len() != 32
|
||||
{
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "ml-dsa", feature = "ed25519-dalek"))]
|
||||
{
|
||||
self.validate_dual_signing()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_to_bytes(&self) -> Result<Zeroizing<Vec<u8>>, crate::error::CryptoError> {
|
||||
let fields: &[&[u8]] = &[
|
||||
self.kem_public_key.as_bytes(),
|
||||
self.kem_secret_key.as_bytes(),
|
||||
|
|
@ -248,10 +332,17 @@ impl Keyring {
|
|||
];
|
||||
let mut out = Zeroizing::new(Vec::new());
|
||||
for f in fields {
|
||||
out.extend_from_slice(&(f.len() as u16).to_be_bytes());
|
||||
let length =
|
||||
u16::try_from(f.len()).map_err(|_| crate::error::CryptoError::InvalidKeyLength)?;
|
||||
out.extend_from_slice(&length.to_be_bytes());
|
||||
out.extend_from_slice(f);
|
||||
}
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
|
||||
self.try_to_bytes()
|
||||
.expect("key material length exceeds wire limit")
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::CryptoError> {
|
||||
|
|
@ -283,6 +374,13 @@ impl Keyring {
|
|||
sig_cl_public_key: SignaturePublicKey::new(read_key(&mut offset)?),
|
||||
sig_cl_secret_key: SignaturePrivateKey::new(read_key(&mut offset)?),
|
||||
})
|
||||
.and_then(|keyring| {
|
||||
if offset == bytes.len() {
|
||||
Ok(keyring)
|
||||
} else {
|
||||
Err(CryptoError::InvalidKeyLength)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_hex(&self) -> String {
|
||||
|
|
@ -352,7 +450,6 @@ impl PublicKeyBundle {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa", feature = "mlkem-tls"))]
|
||||
pub fn validate(&self) -> Result<(), crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
if self.sig_cl_public_key.as_bytes().len() != SIG_CL_PUBLIC_KEY_LEN {
|
||||
|
|
@ -364,25 +461,70 @@ impl PublicKeyBundle {
|
|||
if self.kem_public_key.as_bytes().len() != KEM_PUBLIC_KEY_LEN {
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
#[cfg(feature = "ed25519-dalek")]
|
||||
{
|
||||
let bytes: [u8; SIG_CL_PUBLIC_KEY_LEN] = self
|
||||
.sig_cl_public_key
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
ed25519_dalek::VerifyingKey::from_bytes(&bytes)
|
||||
.map_err(|_| CryptoError::InvalidKeyMaterial)?;
|
||||
}
|
||||
#[cfg(feature = "ml-dsa")]
|
||||
{
|
||||
let encoded = ml_dsa::EncodedVerifyingKey::<ml_dsa::MlDsa65>::try_from(
|
||||
self.sig_pq_public_key.as_bytes(),
|
||||
)
|
||||
.map_err(|_| CryptoError::InvalidKeyMaterial)?;
|
||||
let _ = ml_dsa::VerifyingKey::<ml_dsa::MlDsa65>::decode(&encoded);
|
||||
}
|
||||
#[cfg(feature = "mlkem-tls")]
|
||||
{
|
||||
crate::kem::HybridKem::encapsulate(&self.kem_public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyMaterial)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
pub fn try_as_bytes(&self) -> Result<Vec<u8>, crate::error::CryptoError> {
|
||||
let kem = self.kem_public_key.as_bytes();
|
||||
let pq = self.sig_pq_public_key.as_bytes();
|
||||
let cl = self.sig_cl_public_key.as_bytes();
|
||||
|
||||
let kem_len =
|
||||
u16::try_from(kem.len()).map_err(|_| crate::error::CryptoError::InvalidKeyLength)?;
|
||||
let pq_len =
|
||||
u16::try_from(pq.len()).map_err(|_| crate::error::CryptoError::InvalidKeyLength)?;
|
||||
let cl_len =
|
||||
u16::try_from(cl.len()).map_err(|_| crate::error::CryptoError::InvalidKeyLength)?;
|
||||
let mut out = Vec::with_capacity(kem.len() + pq.len() + cl.len() + 6);
|
||||
out.extend_from_slice(&(kem.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&kem_len.to_be_bytes());
|
||||
out.extend_from_slice(kem);
|
||||
out.extend_from_slice(&(pq.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&pq_len.to_be_bytes());
|
||||
out.extend_from_slice(pq);
|
||||
out.extend_from_slice(&(cl.len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&cl_len.to_be_bytes());
|
||||
out.extend_from_slice(cl);
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
self.try_as_bytes()
|
||||
.expect("public key bundle field exceeds wire limit")
|
||||
}
|
||||
|
||||
/// Parse a complete suite-compatible public bundle.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::CryptoError> {
|
||||
let bundle = Self::from_bytes_unvalidated(bytes)?;
|
||||
bundle.validate()?;
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// Parse the canonical field layout without requiring all suite fields.
|
||||
///
|
||||
/// This is reserved for explicitly partial development material, such as
|
||||
/// an Ed25519-only browser keyring. Callers that will encrypt or verify
|
||||
/// cryptographic protocol values must use [`Self::from_bytes`].
|
||||
pub fn from_bytes_unvalidated(bytes: &[u8]) -> Result<Self, crate::error::CryptoError> {
|
||||
use crate::error::CryptoError;
|
||||
let mut offset = 0;
|
||||
|
||||
|
|
@ -422,6 +564,11 @@ impl PublicKeyBundle {
|
|||
.ok_or(CryptoError::InvalidKeyLength)?
|
||||
.to_vec(),
|
||||
);
|
||||
offset += cl_len;
|
||||
|
||||
if offset != bytes.len() {
|
||||
return Err(CryptoError::InvalidKeyLength);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
kem_public_key: kem,
|
||||
|
|
@ -430,6 +577,11 @@ impl PublicKeyBundle {
|
|||
})
|
||||
}
|
||||
|
||||
/// Parse a complete, suite-compatible public bundle.
|
||||
pub fn from_bytes_validated(bytes: &[u8]) -> Result<Self, crate::error::CryptoError> {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
bytes_to_base64(&self.as_bytes())
|
||||
}
|
||||
|
|
@ -437,6 +589,10 @@ impl PublicKeyBundle {
|
|||
pub fn from_base64(s: &str) -> Result<Self, crate::error::CryptoError> {
|
||||
Self::from_bytes(&base64_to_bytes(s)?)
|
||||
}
|
||||
|
||||
pub fn from_base64_unvalidated(s: &str) -> Result<Self, crate::error::CryptoError> {
|
||||
Self::from_bytes_unvalidated(&base64_to_bytes(s)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for PublicKeyBundle {
|
||||
|
|
@ -474,7 +630,7 @@ mod tests {
|
|||
|
||||
let bundle = PublicKeyBundle::new(kem, pq, cl);
|
||||
let bytes = bundle.as_bytes();
|
||||
let recovered = PublicKeyBundle::from_bytes(&bytes)?;
|
||||
let recovered = PublicKeyBundle::from_bytes_unvalidated(&bytes)?;
|
||||
|
||||
assert_eq!(
|
||||
bundle.kem_public_key.as_bytes(),
|
||||
|
|
@ -491,6 +647,24 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "ml-dsa", feature = "ed25519-dalek"))]
|
||||
#[test]
|
||||
fn full_keyring_validation_checks_key_correspondence() {
|
||||
let keyring = Keyring::generate();
|
||||
assert!(keyring.validate_full().is_ok());
|
||||
assert!(keyring.validate_encryption().is_ok());
|
||||
|
||||
let mut invalid = Keyring::generate();
|
||||
invalid.sig_cl_public_key = SignaturePublicKey::new(vec![0; SIG_CL_PUBLIC_KEY_LEN]);
|
||||
assert!(matches!(
|
||||
invalid.validate_full(),
|
||||
Err(crate::error::CryptoError::InvalidKeyMaterial)
|
||||
));
|
||||
|
||||
invalid.kem_secret_key = KemPrivateKey::new(vec![0]);
|
||||
assert!(invalid.validate_encryption().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_bundle_try_from_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bundle = PublicKeyBundle::new(
|
||||
|
|
@ -499,7 +673,7 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![0xEFu8; 32]),
|
||||
);
|
||||
let bytes: Vec<u8> = Vec::from(&bundle);
|
||||
let recovered = PublicKeyBundle::try_from(bytes.as_slice())?;
|
||||
let recovered = PublicKeyBundle::from_bytes_unvalidated(bytes.as_slice())?;
|
||||
assert_eq!(bundle.as_bytes(), recovered.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -531,6 +705,53 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyring_try_to_bytes_rejects_fields_larger_than_wire_length() {
|
||||
let keyring = Keyring::new(
|
||||
KemPublicKey::new(vec![0u8; 65_536]),
|
||||
KemPrivateKey::new(Vec::new()),
|
||||
SignaturePqPublicKey::new(Vec::new()),
|
||||
SignaturePqPrivateKey::new(Vec::new()),
|
||||
SignaturePublicKey::new(Vec::new()),
|
||||
SignaturePrivateKey::new(Vec::new()),
|
||||
);
|
||||
assert!(matches!(
|
||||
keyring.try_to_bytes(),
|
||||
Err(crate::error::CryptoError::InvalidKeyLength)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_key_parsers_reject_trailing_bytes() {
|
||||
let keyring = Keyring::new(
|
||||
KemPublicKey::new(vec![1u8; 16]),
|
||||
KemPrivateKey::new(vec![2u8; 16]),
|
||||
SignaturePqPublicKey::new(vec![3u8; 16]),
|
||||
SignaturePqPrivateKey::new(vec![4u8; 16]),
|
||||
SignaturePublicKey::new(vec![5u8; 16]),
|
||||
SignaturePrivateKey::new(vec![6u8; 16]),
|
||||
);
|
||||
let mut keyring_bytes = keyring.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();
|
||||
bundle_bytes.push(0xBB);
|
||||
assert!(PublicKeyBundle::from_bytes(&bundle_bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_bundle_rejects_partial_suite_keys() {
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyring_try_from_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let keyring = Keyring::new(
|
||||
|
|
@ -597,7 +818,7 @@ mod tests {
|
|||
SignaturePublicKey::new(vec![3u8; 32]),
|
||||
);
|
||||
let b64 = bundle.to_base64();
|
||||
let recovered = PublicKeyBundle::from_base64(&b64)?;
|
||||
let recovered = PublicKeyBundle::from_base64_unvalidated(&b64)?;
|
||||
assert_eq!(bundle.as_bytes(), recovered.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub use keypair::{
|
|||
};
|
||||
|
||||
#[cfg(feature = "chacha20poly1305")]
|
||||
pub use aead::ChaCha20Poly1305;
|
||||
pub use aead::{ChaCha20Poly1305, XChaCha20Poly1305};
|
||||
|
||||
#[cfg(feature = "aes-gcm")]
|
||||
pub use aead::Aes256Gcm;
|
||||
|
|
@ -55,7 +55,7 @@ pub use sign::{Ed25519Signer, SignatureScheme, verify_ed25519};
|
|||
pub use sign::{MlDsaSigner, verify_ml_dsa};
|
||||
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
pub use sign::{DualSignature, sign_dual};
|
||||
pub use sign::{DualSignature, DualSigner, sign_dual};
|
||||
|
||||
#[cfg(feature = "sha2")]
|
||||
pub use hash::{Sha256Hasher, sha256, sha256_double};
|
||||
|
|
@ -79,11 +79,12 @@ pub fn ensure_crypto_provider() {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub use enc::{decrypt_with, encrypt_for};
|
||||
pub use helper::{ENCRYPT_DOMAIN, KEY_WRAP_DOMAIN};
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
pub use helper::{MultiEncryptedMessage, RecipientEntry, decrypt_multi, encrypt_multi};
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "hkdf"))]
|
||||
pub use helper::{
|
||||
MAX_RECIPIENTS, MultiEncryptedMessage, RecipientEntry, decrypt_multi_for, encrypt_multi_for,
|
||||
};
|
||||
|
||||
/* ================================ TESTS ================================ */
|
||||
#[cfg(test)]
|
||||
|
|
@ -209,6 +210,20 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
#[test]
|
||||
fn dual_scheme_implements_signature_trait() {
|
||||
use crate::sign::SignatureScheme;
|
||||
|
||||
let (signer, _, _, _, _) = DualSigner::generate();
|
||||
let signature = signer.sign(b"msg").expect("dual signing should succeed");
|
||||
assert_eq!(signer.algorithm(), SigAlgorithm::DUAL);
|
||||
signer
|
||||
.verify(b"msg", &signature)
|
||||
.expect("dual verification should succeed");
|
||||
assert!(signer.verify(b"wrong", &signature).is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "hkdf")]
|
||||
#[test]
|
||||
fn hkdf_expand_produces_key() {
|
||||
|
|
@ -343,17 +358,46 @@ mod tests {
|
|||
assert_eq!(enc.shared_secret, ss);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
|
||||
#[test]
|
||||
fn encrypt_multi_roundtrip() {
|
||||
use crate::helper::{decrypt_multi, encrypt_multi};
|
||||
#[cfg(all(
|
||||
feature = "mlkem-tls",
|
||||
feature = "hkdf",
|
||||
feature = "ml-dsa",
|
||||
feature = "ed25519-dalek"
|
||||
))]
|
||||
fn multi_envelope_roundtrip(encryption_type: EncryptionType) {
|
||||
use crate::helper::{decrypt_multi_for, encrypt_multi_for};
|
||||
use crate::keypair::Keyring;
|
||||
|
||||
let kr = Keyring::generate();
|
||||
let entities = vec![kr.public_key_bundle()];
|
||||
let msg = b"secret data";
|
||||
let ct = encrypt_multi(msg, b"aad", &entities).expect("multi encrypt should succeed");
|
||||
let pt = decrypt_multi(&ct, b"aad", &kr).expect("multi decrypt should succeed");
|
||||
let ct = encrypt_multi_for(encryption_type, 7, msg, &entities)
|
||||
.expect("multi encrypt should succeed");
|
||||
let pt = decrypt_multi_for(&ct, 7, &kr).expect("multi decrypt should succeed");
|
||||
assert_eq!(pt, msg);
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "mlkem-tls",
|
||||
feature = "hkdf",
|
||||
feature = "ml-dsa",
|
||||
feature = "ed25519-dalek",
|
||||
feature = "chacha20poly1305"
|
||||
))]
|
||||
#[test]
|
||||
fn chacha20_multi_envelope_roundtrip() {
|
||||
multi_envelope_roundtrip(EncryptionType::MlKemChaCha20Poly1305);
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "mlkem-tls",
|
||||
feature = "hkdf",
|
||||
feature = "ml-dsa",
|
||||
feature = "ed25519-dalek",
|
||||
feature = "aes-gcm"
|
||||
))]
|
||||
#[test]
|
||||
fn aes_gcm_multi_envelope_roundtrip() {
|
||||
multi_envelope_roundtrip(EncryptionType::MlKemAes256Gcm);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ impl SigAlgorithm {
|
|||
use crate::keypair::{SignaturePqPrivateKey, SignaturePqPublicKey};
|
||||
|
||||
pub trait SignatureScheme {
|
||||
/// The wire algorithm identifier produced by this signer.
|
||||
fn algorithm(&self) -> u8;
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, CryptoError>;
|
||||
fn verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), CryptoError>;
|
||||
}
|
||||
|
|
@ -76,6 +78,10 @@ impl Ed25519Signer {
|
|||
|
||||
#[cfg(feature = "ed25519-dalek")]
|
||||
impl SignatureScheme for Ed25519Signer {
|
||||
fn algorithm(&self) -> u8 {
|
||||
SigAlgorithm::ED25519
|
||||
}
|
||||
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
use ed25519_dalek::Signer;
|
||||
let signature = self.secret.sign(msg).to_bytes().to_vec();
|
||||
|
|
@ -170,6 +176,10 @@ impl MlDsaSigner {
|
|||
|
||||
#[cfg(feature = "ml-dsa")]
|
||||
impl SignatureScheme for MlDsaSigner {
|
||||
fn algorithm(&self) -> u8 {
|
||||
SigAlgorithm::ML_DSA_65
|
||||
}
|
||||
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
use ml_dsa::Signer;
|
||||
let signature = self
|
||||
|
|
@ -237,6 +247,75 @@ pub fn sign_dual(
|
|||
Ok(DualSignature { ed25519, mldsa })
|
||||
}
|
||||
|
||||
/// A signer that produces the canonical concatenated Ed25519 + ML-DSA-65
|
||||
/// signature represented by [`SigAlgorithm::DUAL`].
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
pub struct DualSigner {
|
||||
ed25519: Ed25519Signer,
|
||||
mldsa: MlDsaSigner,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
impl DualSigner {
|
||||
pub fn new(
|
||||
ed25519_secret: &SignaturePrivateKey,
|
||||
mldsa_secret: &SignaturePqPrivateKey,
|
||||
mldsa_public: &SignaturePqPublicKey,
|
||||
) -> Result<Self, CryptoError> {
|
||||
Ok(Self {
|
||||
ed25519: Ed25519Signer::new(ed25519_secret)?,
|
||||
mldsa: MlDsaSigner::new(mldsa_secret, mldsa_public)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate() -> (
|
||||
Self,
|
||||
SignaturePrivateKey,
|
||||
SignaturePqPrivateKey,
|
||||
SignaturePublicKey,
|
||||
SignaturePqPublicKey,
|
||||
) {
|
||||
let (ed25519, ed25519_secret, ed25519_public) = Ed25519Signer::generate();
|
||||
let (mldsa, mldsa_secret, mldsa_public) = MlDsaSigner::generate();
|
||||
(
|
||||
Self { ed25519, mldsa },
|
||||
ed25519_secret,
|
||||
mldsa_secret,
|
||||
ed25519_public,
|
||||
mldsa_public,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
impl SignatureScheme for DualSigner {
|
||||
fn algorithm(&self) -> u8 {
|
||||
SigAlgorithm::DUAL
|
||||
}
|
||||
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let dual = sign_dual(self.ed25519.signing_key(), self.mldsa.signing_key(), msg)?;
|
||||
let mut signature = Vec::with_capacity(
|
||||
SigAlgorithm::length(SigAlgorithm::DUAL).expect("known signature algorithm length"),
|
||||
);
|
||||
signature.extend_from_slice(&dual.ed25519);
|
||||
signature.extend_from_slice(&dual.mldsa);
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
fn verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
|
||||
let ed_len =
|
||||
SigAlgorithm::length(SigAlgorithm::ED25519).expect("known signature algorithm length");
|
||||
let mldsa_len = SigAlgorithm::length(SigAlgorithm::ML_DSA_65)
|
||||
.expect("known signature algorithm length");
|
||||
if signature.len() != ed_len + mldsa_len {
|
||||
return Err(CryptoError::InvalidSignature);
|
||||
}
|
||||
verify_ed25519(&self.ed25519.public_key(), msg, &signature[..ed_len])?;
|
||||
verify_ml_dsa(&self.mldsa.public_key(), msg, &signature[ed_len..])
|
||||
}
|
||||
}
|
||||
|
||||
impl DualSignature {
|
||||
#[cfg(all(feature = "ed25519-dalek", feature = "ml-dsa"))]
|
||||
pub fn verify(
|
||||
|
|
|
|||
Loading…
Reference in a new issue