#[derive(Debug, Clone, Copy)]
pub enum TradKemAlg {
Rsa {
bits: u32,
},
Ec { curve: &'static str },
X25519,
X448,
}
impl TradKemAlg {
pub fn raw_len(&self) -> usize {
match self {
TradKemAlg::Rsa { bits } => (*bits as usize) / 8,
TradKemAlg::Ec { curve } => match *curve {
"P-256" => 65,
"P-384" => 97,
"P-521" => 133,
"brainpoolP256r1" => 65,
"brainpoolP384r1" => 97,
other => panic!("composite KEM: unsupported EC curve {other}"),
},
TradKemAlg::X25519 => 32,
TradKemAlg::X448 => 56,
}
}
}
const fn sub_arc_of(oid: &[u32]) -> u32 {
oid[oid.len() - 1]
}
pub const SUB_ARC_MLKEM768_RSA2048: u32 = sub_arc_of(crate::oids::MLKEM768_RSA2048_SHA3_256);
pub const SUB_ARC_MLKEM768_RSA3072: u32 = sub_arc_of(crate::oids::MLKEM768_RSA3072_SHA3_256);
pub const SUB_ARC_MLKEM768_RSA4096: u32 = sub_arc_of(crate::oids::MLKEM768_RSA4096_SHA3_256);
pub const SUB_ARC_MLKEM768_X25519: u32 = sub_arc_of(crate::oids::MLKEM768_X25519_SHA3_256);
pub const SUB_ARC_MLKEM768_ECDH_P256: u32 = sub_arc_of(crate::oids::MLKEM768_ECDH_P256_SHA3_256);
pub const SUB_ARC_MLKEM768_ECDH_P384: u32 = sub_arc_of(crate::oids::MLKEM768_ECDH_P384_SHA3_256);
pub const SUB_ARC_MLKEM768_ECDH_BRAINPOOL_P256R1: u32 =
sub_arc_of(crate::oids::MLKEM768_ECDH_BRAINPOOL_P256R1_SHA3_256);
pub const SUB_ARC_MLKEM1024_RSA3072: u32 = sub_arc_of(crate::oids::MLKEM1024_RSA3072_SHA3_256);
pub const SUB_ARC_MLKEM1024_ECDH_P384: u32 = sub_arc_of(crate::oids::MLKEM1024_ECDH_P384_SHA3_256);
pub const SUB_ARC_MLKEM1024_ECDH_BRAINPOOL_P384R1: u32 =
sub_arc_of(crate::oids::MLKEM1024_ECDH_BRAINPOOL_P384R1_SHA3_256);
pub const SUB_ARC_MLKEM1024_X448: u32 = sub_arc_of(crate::oids::MLKEM1024_X448_SHA3_256);
pub const SUB_ARC_MLKEM1024_ECDH_P521: u32 = sub_arc_of(crate::oids::MLKEM1024_ECDH_P521_SHA3_256);
#[derive(Debug)]
pub struct CompositeKemSpec {
pub sub_arc: u32,
pub mlkem_variant: &'static str,
pub mlkem_pk_size: usize,
pub mlkem_ct_size: usize,
pub trad_alg: TradKemAlg,
pub label: &'static [u8],
}
static COMPOSITE_SPECS: &[CompositeKemSpec] = &[
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_RSA2048,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Rsa { bits: 2048 },
label: b"MLKEM768-RSAOAEP2048",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_RSA3072,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Rsa { bits: 3072 },
label: b"MLKEM768-RSAOAEP3072",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_RSA4096,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Rsa { bits: 4096 },
label: b"MLKEM768-RSAOAEP4096",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_X25519,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::X25519,
label: &[0x5c, 0x2e, 0x2f, 0x2f, 0x5e, 0x5c],
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_ECDH_P256,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Ec { curve: "P-256" },
label: b"MLKEM768-P256",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_ECDH_P384,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Ec { curve: "P-384" },
label: b"MLKEM768-P384",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM768_ECDH_BRAINPOOL_P256R1,
mlkem_variant: "ML-KEM-768",
mlkem_pk_size: 1184,
mlkem_ct_size: 1088,
trad_alg: TradKemAlg::Ec {
curve: "brainpoolP256r1",
},
label: b"MLKEM768-BP256",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM1024_RSA3072,
mlkem_variant: "ML-KEM-1024",
mlkem_pk_size: 1568,
mlkem_ct_size: 1568,
trad_alg: TradKemAlg::Rsa { bits: 3072 },
label: b"MLKEM1024-RSAOAEP3072",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM1024_ECDH_P384,
mlkem_variant: "ML-KEM-1024",
mlkem_pk_size: 1568,
mlkem_ct_size: 1568,
trad_alg: TradKemAlg::Ec { curve: "P-384" },
label: b"MLKEM1024-P384",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM1024_ECDH_BRAINPOOL_P384R1,
mlkem_variant: "ML-KEM-1024",
mlkem_pk_size: 1568,
mlkem_ct_size: 1568,
trad_alg: TradKemAlg::Ec {
curve: "brainpoolP384r1",
},
label: b"MLKEM1024-BP384",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM1024_X448,
mlkem_variant: "ML-KEM-1024",
mlkem_pk_size: 1568,
mlkem_ct_size: 1568,
trad_alg: TradKemAlg::X448,
label: b"MLKEM1024-X448",
},
CompositeKemSpec {
sub_arc: SUB_ARC_MLKEM1024_ECDH_P521,
mlkem_variant: "ML-KEM-1024",
mlkem_pk_size: 1568,
mlkem_ct_size: 1568,
trad_alg: TradKemAlg::Ec { curve: "P-521" },
label: b"MLKEM1024-P521",
},
];
pub fn composite_spec(sub_arc: u32) -> Option<&'static CompositeKemSpec> {
COMPOSITE_SPECS.iter().find(|s| s.sub_arc == sub_arc)
}
pub fn composite_spec_from_oid(comps: &[u32]) -> Option<&'static CompositeKemSpec> {
let arc = crate::oids::COMPOSITE_KEM_ARC;
if comps.len() != arc.len() + 1 {
return None;
}
if !comps[..arc.len()]
.iter()
.zip(arc.iter())
.all(|(a, b)| a == b)
{
return None;
}
composite_spec(comps[arc.len()])
}
pub fn combiner_input(
mlkem_ss: &[u8],
trad_ss: &[u8],
trad_ct: &[u8],
trad_pk: &[u8],
label: &[u8],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(
mlkem_ss.len() + trad_ss.len() + trad_ct.len() + trad_pk.len() + label.len(),
);
buf.extend_from_slice(mlkem_ss);
buf.extend_from_slice(trad_ss);
buf.extend_from_slice(trad_ct);
buf.extend_from_slice(trad_pk);
buf.extend_from_slice(label);
buf
}
pub fn split_composite_kem_spki_content<'a>(
payload: &'a [u8],
spec: &CompositeKemSpec,
) -> Result<(&'a [u8], &'a [u8]), String> {
if payload.len() < spec.mlkem_pk_size {
return Err(format!(
"composite KEM SPKI payload too short for sub-arc {} (mlkem_pk_size={}): got {} bytes",
spec.sub_arc,
spec.mlkem_pk_size,
payload.len()
));
}
Ok(payload.split_at(spec.mlkem_pk_size))
}
pub fn split_composite_kem_ciphertext<'a>(
ct: &'a [u8],
spec: &CompositeKemSpec,
) -> Result<(&'a [u8], &'a [u8]), String> {
if ct.len() < spec.mlkem_ct_size {
return Err(format!(
"composite KEM ciphertext too short for sub-arc {} (mlkem_ct_size={}): got {} bytes",
spec.sub_arc,
spec.mlkem_ct_size,
ct.len()
));
}
Ok(ct.split_at(spec.mlkem_ct_size))
}
pub fn split_composite_kem_privkey(privkey_content: &[u8]) -> Result<(&[u8], &[u8]), String> {
const SEED_LEN: usize = 64;
if privkey_content.len() <= SEED_LEN {
return Err(format!(
"composite KEM private key content too short: {} <= {} (no traditional key material)",
privkey_content.len(),
SEED_LEN
));
}
Ok(privkey_content.split_at(SEED_LEN))
}