pub const CASCADE_OUTER: &[u8] = b"LatticeArc-v1-Cascade-ChaCha20Poly1305";
pub const CASCADE_INNER: &[u8] = b"LatticeArc-v1-Cascade-AES256GCM";
pub const HYBRID_ENCRYPTION_INFO: &[u8] = b"LatticeArc-Hybrid-Encryption-v1";
pub const HYBRID_KEM_SS_INFO: &[u8] = b"LatticeArc-Hybrid-KEM-SS-v1";
pub const DERIVE_KEY_INFO: &[u8] = b"LatticeArc-DeriveKey-v1";
pub const MODULE_INTEGRITY_HMAC_KEY: &[u8] = b"LatticeArc-FIPS-140-3-Module-Integrity-Key-v1";
pub const PQ_KEM_AEAD_KEY_INFO: &[u8] = b"LatticeArc-PqKem-AeadKey-v1";
pub const PQ_ONLY_ENCRYPTION_INFO: &[u8] = b"LatticeArc-PqOnly-Encryption-v1";
#[derive(Clone, Copy, Debug)]
pub(crate) enum HkdfKemLabel {
PqKemAead,
PqOnlyEncryption,
}
impl HkdfKemLabel {
fn as_bytes(self) -> &'static [u8] {
match self {
Self::PqKemAead => PQ_KEM_AEAD_KEY_INFO,
Self::PqOnlyEncryption => PQ_ONLY_ENCRYPTION_INFO,
}
}
}
pub(crate) fn hkdf_kem_info_with_pk(
label: HkdfKemLabel,
recipient_pk: &[u8],
kem_ciphertext: &[u8],
) -> Result<Vec<u8>, crate::prelude::error::LatticeArcError> {
let label_bytes = label.as_bytes();
debug_assert!(
!label_bytes.contains(&0x00),
"HkdfKemLabel::as_bytes() must be NUL-free; the 0x00 separator below would collide"
);
if label_bytes.contains(&0x00) {
return Err(crate::prelude::error::LatticeArcError::ValidationError {
message: "HKDF label contains a NUL byte; would collide with the domain separator"
.to_string(),
});
}
let cap = label_bytes
.len()
.saturating_add(1)
.saturating_add(4)
.saturating_add(recipient_pk.len())
.saturating_add(4)
.saturating_add(kem_ciphertext.len());
let mut info = Vec::with_capacity(cap);
info.extend_from_slice(label_bytes);
info.push(0x00); let pk_len_u32 = u32::try_from(recipient_pk.len()).map_err(|_overflow| {
crate::prelude::error::LatticeArcError::InvalidInput(
"recipient PK exceeds 4 GiB".to_string(),
)
})?;
info.extend_from_slice(&pk_len_u32.to_be_bytes());
info.extend_from_slice(recipient_pk);
let ct_len_u32 = u32::try_from(kem_ciphertext.len()).map_err(|_overflow| {
crate::prelude::error::LatticeArcError::InvalidInput(
"KEM ciphertext exceeds 4 GiB".to_string(),
)
})?;
info.extend_from_slice(&ct_len_u32.to_be_bytes());
info.extend_from_slice(kem_ciphertext);
Ok(info)
}
#[cfg(test)]
mod hkdf_kem_label_tests {
use super::*;
#[test]
fn all_label_variants_are_nul_free() {
for label in [HkdfKemLabel::PqKemAead, HkdfKemLabel::PqOnlyEncryption] {
let bytes = label.as_bytes();
assert!(
!bytes.contains(&0u8),
"HkdfKemLabel::{:?} maps to a byte string containing 0x00 \
({:?}) — this breaks the NUL separator in hkdf_kem_info_with_pk",
label,
bytes,
);
}
}
}
#[cfg(kani)]
#[expect(
clippy::indexing_slicing,
reason = "indexing into a slice whose length is known at this site"
)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn domain_constants_pairwise_distinct() {
let constants: &[&[u8]] = &[
CASCADE_OUTER,
CASCADE_INNER,
HYBRID_ENCRYPTION_INFO,
HYBRID_KEM_SS_INFO,
DERIVE_KEY_INFO,
MODULE_INTEGRITY_HMAC_KEY,
PQ_KEM_AEAD_KEY_INFO,
PQ_ONLY_ENCRYPTION_INFO,
];
let n = constants.len();
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n {
kani::assert(
constants[i] != constants[j],
"All HKDF domain constants must be pairwise distinct",
);
j += 1;
}
i += 1;
}
}
}