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";
pub(crate) const SIG_CONTEXT_ML_DSA_44: &[u8] = b"LatticeArc-Sig-ml-dsa-44-v1";
pub(crate) const SIG_CONTEXT_ML_DSA_65: &[u8] = b"LatticeArc-Sig-ml-dsa-65-v1";
pub(crate) const SIG_CONTEXT_ML_DSA_87: &[u8] = b"LatticeArc-Sig-ml-dsa-87-v1";
pub(crate) const SIG_CONTEXT_SLH_DSA_SHAKE_128S: &[u8] = b"LatticeArc-Sig-slh-dsa-shake-128s-v1";
pub(crate) const SIG_CONTEXT_SLH_DSA_SHAKE_192S: &[u8] = b"LatticeArc-Sig-slh-dsa-shake-192s-v1";
pub(crate) const SIG_CONTEXT_SLH_DSA_SHAKE_256S: &[u8] = b"LatticeArc-Sig-slh-dsa-shake-256s-v1";
pub(crate) const SIG_CONTEXT_FN_DSA_512: &[u8] = b"LatticeArc-Sig-fn-dsa-512-v1";
pub(crate) const SIG_CONTEXT_FN_DSA_1024: &[u8] = b"LatticeArc-Sig-fn-dsa-1024-v1";
pub(crate) const SIG_CONTEXT_HYBRID_ML_DSA_44_ED25519: &[u8] =
b"LatticeArc-Sig-hybrid-ml-dsa-44-ed25519-v1";
pub(crate) const SIG_CONTEXT_HYBRID_ML_DSA_65_ED25519: &[u8] =
b"LatticeArc-Sig-hybrid-ml-dsa-65-ed25519-v1";
pub(crate) const SIG_CONTEXT_HYBRID_ML_DSA_87_ED25519: &[u8] =
b"LatticeArc-Sig-hybrid-ml-dsa-87-ed25519-v1";
#[cfg(not(feature = "fips"))]
pub(crate) const SIG_CONTEXT_ED25519: &[u8] = b"LatticeArc-Sig-ed25519-v1";
pub(crate) const SIG_CONTEXT_POP_ED25519: &[u8] = b"LatticeArc-PoP-ed25519-v1";
pub(crate) const SIG_CONTEXT_ZK_PROOF_ED25519: &[u8] = b"LatticeArc-ZK-Proof-ed25519-v1";
pub(crate) const SP800_108_LABEL_ENCRYPTION: &[u8] = b"Encryption Key";
pub(crate) const SP800_108_LABEL_MAC: &[u8] = b"MAC Key";
pub(crate) const SP800_108_LABEL_IV: &[u8] = b"IV Generation";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SigSchemeLabel {
MlDsa44,
MlDsa65,
MlDsa87,
SlhDsaShake128s,
SlhDsaShake192s,
SlhDsaShake256s,
FnDsa512,
FnDsa1024,
HybridMlDsa44Ed25519,
HybridMlDsa65Ed25519,
HybridMlDsa87Ed25519,
#[cfg(not(feature = "fips"))]
Ed25519,
}
impl SigSchemeLabel {
pub(crate) const fn as_bytes(self) -> &'static [u8] {
match self {
Self::MlDsa44 => SIG_CONTEXT_ML_DSA_44,
Self::MlDsa65 => SIG_CONTEXT_ML_DSA_65,
Self::MlDsa87 => SIG_CONTEXT_ML_DSA_87,
Self::SlhDsaShake128s => SIG_CONTEXT_SLH_DSA_SHAKE_128S,
Self::SlhDsaShake192s => SIG_CONTEXT_SLH_DSA_SHAKE_192S,
Self::SlhDsaShake256s => SIG_CONTEXT_SLH_DSA_SHAKE_256S,
Self::FnDsa512 => SIG_CONTEXT_FN_DSA_512,
Self::FnDsa1024 => SIG_CONTEXT_FN_DSA_1024,
Self::HybridMlDsa44Ed25519 => SIG_CONTEXT_HYBRID_ML_DSA_44_ED25519,
Self::HybridMlDsa65Ed25519 => SIG_CONTEXT_HYBRID_ML_DSA_65_ED25519,
Self::HybridMlDsa87Ed25519 => SIG_CONTEXT_HYBRID_ML_DSA_87_ED25519,
#[cfg(not(feature = "fips"))]
Self::Ed25519 => SIG_CONTEXT_ED25519,
}
}
pub(crate) fn from_scheme_str(scheme: &str) -> Option<Self> {
match scheme {
"ml-dsa-44" | "pq-ml-dsa-44" => Some(Self::MlDsa44),
"ml-dsa-65" | "pq-ml-dsa-65" => Some(Self::MlDsa65),
"ml-dsa-87" | "pq-ml-dsa-87" => Some(Self::MlDsa87),
"slh-dsa-shake-128s" => Some(Self::SlhDsaShake128s),
"slh-dsa-shake-192s" => Some(Self::SlhDsaShake192s),
"slh-dsa-shake-256s" => Some(Self::SlhDsaShake256s),
"fn-dsa-512" | "fn-dsa" => Some(Self::FnDsa512),
"fn-dsa-1024" => Some(Self::FnDsa1024),
"hybrid-ml-dsa-44-ed25519" | "ml-dsa-44-hybrid-ed25519" => {
Some(Self::HybridMlDsa44Ed25519)
}
"hybrid-ml-dsa-65-ed25519" | "ml-dsa-65-hybrid-ed25519" => {
Some(Self::HybridMlDsa65Ed25519)
}
"hybrid-ml-dsa-87-ed25519" | "ml-dsa-87-hybrid-ed25519" => {
Some(Self::HybridMlDsa87Ed25519)
}
#[cfg(not(feature = "fips"))]
"ed25519" => Some(Self::Ed25519),
_ => None,
}
}
}
#[inline]
pub(crate) const fn sig_context(label: SigSchemeLabel) -> &'static [u8] {
label.as_bytes()
}
#[inline]
pub(crate) const fn pop_sig_context() -> &'static [u8] {
SIG_CONTEXT_POP_ED25519
}
#[inline]
pub(crate) const fn zk_proof_sig_context() -> &'static [u8] {
SIG_CONTEXT_ZK_PROOF_ED25519
}
pub(crate) fn hash_with_context(scheme_ctx: &[u8], message: &[u8]) -> [u8; 64] {
use sha2::{Digest, Sha512};
let mut hasher = Sha512::new();
hasher.update(scheme_ctx);
hasher.update([0x00]);
hasher.update(message);
hasher.finalize().into()
}
#[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)
}
pub(crate) fn hkdf_kem_info_with_pk_and_aad(
label: HkdfKemLabel,
aad: &[u8],
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(aad.len())
.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 aad_len_u32 = u32::try_from(aad.len()).map_err(|_overflow| {
crate::prelude::error::LatticeArcError::InvalidInput("AAD exceeds 4 GiB".to_string())
})?;
info.extend_from_slice(&aad_len_u32.to_be_bytes());
info.extend_from_slice(aad);
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(test)]
mod sig_scheme_label_tests {
use super::*;
const ALL_LABELS: &[SigSchemeLabel] = &[
SigSchemeLabel::MlDsa44,
SigSchemeLabel::MlDsa65,
SigSchemeLabel::MlDsa87,
SigSchemeLabel::SlhDsaShake128s,
SigSchemeLabel::SlhDsaShake192s,
SigSchemeLabel::SlhDsaShake256s,
SigSchemeLabel::FnDsa512,
SigSchemeLabel::FnDsa1024,
SigSchemeLabel::HybridMlDsa44Ed25519,
SigSchemeLabel::HybridMlDsa65Ed25519,
SigSchemeLabel::HybridMlDsa87Ed25519,
#[cfg(not(feature = "fips"))]
SigSchemeLabel::Ed25519,
];
#[test]
fn all_sig_context_variants_are_nul_free() {
for &label in ALL_LABELS {
let bytes = label.as_bytes();
assert!(
!bytes.contains(&0u8),
"SigSchemeLabel::{label:?} maps to {bytes:?} which contains 0x00; \
the prefix-padding separator below would collide"
);
}
}
#[test]
fn all_sig_contexts_pairwise_distinct() {
for (i, &a) in ALL_LABELS.iter().enumerate() {
let Some(rest) = ALL_LABELS.get(i.saturating_add(1)..) else {
continue;
};
for &b in rest {
assert_ne!(
a.as_bytes(),
b.as_bytes(),
"SigSchemeLabel::{a:?} and ::{b:?} share a context"
);
}
}
}
#[test]
fn canonical_scheme_strings_round_trip() {
let pairs: &[(&str, SigSchemeLabel)] = &[
("ml-dsa-44", SigSchemeLabel::MlDsa44),
("ml-dsa-65", SigSchemeLabel::MlDsa65),
("ml-dsa-87", SigSchemeLabel::MlDsa87),
("pq-ml-dsa-44", SigSchemeLabel::MlDsa44),
("pq-ml-dsa-65", SigSchemeLabel::MlDsa65),
("pq-ml-dsa-87", SigSchemeLabel::MlDsa87),
("slh-dsa-shake-128s", SigSchemeLabel::SlhDsaShake128s),
("slh-dsa-shake-192s", SigSchemeLabel::SlhDsaShake192s),
("slh-dsa-shake-256s", SigSchemeLabel::SlhDsaShake256s),
("fn-dsa-512", SigSchemeLabel::FnDsa512),
("fn-dsa", SigSchemeLabel::FnDsa512),
("fn-dsa-1024", SigSchemeLabel::FnDsa1024),
("hybrid-ml-dsa-44-ed25519", SigSchemeLabel::HybridMlDsa44Ed25519),
("ml-dsa-44-hybrid-ed25519", SigSchemeLabel::HybridMlDsa44Ed25519),
("hybrid-ml-dsa-65-ed25519", SigSchemeLabel::HybridMlDsa65Ed25519),
("ml-dsa-65-hybrid-ed25519", SigSchemeLabel::HybridMlDsa65Ed25519),
("hybrid-ml-dsa-87-ed25519", SigSchemeLabel::HybridMlDsa87Ed25519),
("ml-dsa-87-hybrid-ed25519", SigSchemeLabel::HybridMlDsa87Ed25519),
#[cfg(not(feature = "fips"))]
("ed25519", SigSchemeLabel::Ed25519),
];
for &(s, want) in pairs {
assert_eq!(
SigSchemeLabel::from_scheme_str(s),
Some(want),
"scheme string {s:?} did not map to {want:?}"
);
}
}
#[test]
fn unknown_scheme_strings_rejected() {
for s in [
"",
"ml-dsa-99",
"rsa-2048",
"hybrid-ml-dsa-65", "ml-dsa-65-hybrid", "hybrid-ml-dsa-65-ed25519-extra", "Ml-Dsa-65", ] {
assert_eq!(
SigSchemeLabel::from_scheme_str(s),
None,
"scheme string {s:?} must be rejected by the M5 allowlist"
);
}
}
#[cfg(feature = "fips")]
#[test]
fn fips_rejects_pure_ed25519_at_allowlist() {
assert_eq!(
SigSchemeLabel::from_scheme_str("ed25519"),
None,
"pure ed25519 must be rejected by M5 under --features fips"
);
}
#[test]
fn extra_ed25519_contexts_pairwise_distinct_from_sig_scheme_contexts() {
let pop = pop_sig_context();
let zk = zk_proof_sig_context();
assert_ne!(pop, zk, "PoP and ZK contexts must differ");
assert!(!pop.contains(&0u8), "PoP context must be NUL-free for prefix-padding");
assert!(!zk.contains(&0u8), "ZK context must be NUL-free for prefix-padding");
for &label in ALL_LABELS {
assert_ne!(
label.as_bytes(),
pop,
"PoP context collides with SigSchemeLabel::{label:?}"
);
assert_ne!(label.as_bytes(), zk, "ZK context collides with SigSchemeLabel::{label:?}");
}
}
}
#[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;
}
}
}