#[derive(Debug, Clone, Copy)]
pub enum TradAlg {
RsaPss { bits: u32, hash: &'static str },
RsaPkcs15 { bits: u32, hash: &'static str },
Ec {
curve: &'static str,
hash: &'static str,
},
Ed25519,
Ed448,
}
#[derive(Debug, Clone, Copy)]
pub enum CompHash {
Sha256,
Sha512,
Shake256_64,
}
#[derive(Debug)]
pub struct CompositeMlDsaSpec {
pub sub_arc: u32,
pub mldsa_variant: &'static str,
pub mldsa_pk_size: usize,
pub mldsa_sig_size: usize,
pub trad_alg: TradAlg,
pub hash: CompHash,
pub label: &'static str,
}
static COMPOSITE_SPECS: &[CompositeMlDsaSpec] = &[
CompositeMlDsaSpec {
sub_arc: 37,
mldsa_variant: "ML-DSA-44",
mldsa_pk_size: 1312,
mldsa_sig_size: 2420,
trad_alg: TradAlg::RsaPss {
bits: 2048,
hash: "sha256",
},
hash: CompHash::Sha256,
label: "COMPSIG-MLDSA44-RSA2048-PSS-SHA256",
},
CompositeMlDsaSpec {
sub_arc: 38,
mldsa_variant: "ML-DSA-44",
mldsa_pk_size: 1312,
mldsa_sig_size: 2420,
trad_alg: TradAlg::RsaPkcs15 {
bits: 2048,
hash: "sha256",
},
hash: CompHash::Sha256,
label: "COMPSIG-MLDSA44-RSA2048-PKCS15-SHA256",
},
CompositeMlDsaSpec {
sub_arc: 39,
mldsa_variant: "ML-DSA-44",
mldsa_pk_size: 1312,
mldsa_sig_size: 2420,
trad_alg: TradAlg::Ed25519,
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA44-Ed25519-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 40,
mldsa_variant: "ML-DSA-44",
mldsa_pk_size: 1312,
mldsa_sig_size: 2420,
trad_alg: TradAlg::Ec {
curve: "P-256",
hash: "sha256",
},
hash: CompHash::Sha256,
label: "COMPSIG-MLDSA44-ECDSA-P256-SHA256",
},
CompositeMlDsaSpec {
sub_arc: 41,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::RsaPss {
bits: 3072,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-RSA3072-PSS-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 42,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::RsaPkcs15 {
bits: 3072,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-RSA3072-PKCS15-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 43,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::RsaPss {
bits: 4096,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-RSA4096-PSS-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 44,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::RsaPkcs15 {
bits: 4096,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-RSA4096-PKCS15-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 45,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::Ec {
curve: "P-256",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-ECDSA-P256-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 46,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::Ec {
curve: "P-384",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-ECDSA-P384-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 47,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::Ec {
curve: "brainpoolP256r1",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-ECDSA-BP256-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 48,
mldsa_variant: "ML-DSA-65",
mldsa_pk_size: 1952,
mldsa_sig_size: 3309,
trad_alg: TradAlg::Ed25519,
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA65-Ed25519-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 49,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::Ec {
curve: "P-384",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA87-ECDSA-P384-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 50,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::Ec {
curve: "brainpoolP384r1",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA87-ECDSA-BP384-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 51,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::Ed448,
hash: CompHash::Shake256_64,
label: "COMPSIG-MLDSA87-Ed448-SHAKE256",
},
CompositeMlDsaSpec {
sub_arc: 52,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::RsaPss {
bits: 3072,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA87-RSA3072-PSS-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 53,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::RsaPss {
bits: 4096,
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA87-RSA4096-PSS-SHA512",
},
CompositeMlDsaSpec {
sub_arc: 54,
mldsa_variant: "ML-DSA-87",
mldsa_pk_size: 2592,
mldsa_sig_size: 4627,
trad_alg: TradAlg::Ec {
curve: "P-521",
hash: "sha512",
},
hash: CompHash::Sha512,
label: "COMPSIG-MLDSA87-ECDSA-P521-SHA512",
},
];
pub fn composite_spec(sub_arc: u32) -> Option<&'static CompositeMlDsaSpec> {
const FIRST: u32 = 37;
debug_assert!(
COMPOSITE_SPECS
.iter()
.enumerate()
.all(|(i, s)| s.sub_arc == FIRST + i as u32),
"COMPOSITE_SPECS table is not sorted by sub_arc starting at 37"
);
let idx = sub_arc.checked_sub(FIRST)? as usize;
COMPOSITE_SPECS.get(idx)
}
pub fn composite_spec_from_oid(comps: &[u32]) -> Option<&'static CompositeMlDsaSpec> {
let arc = crate::oids::COMPOSITE_MLDSA_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 build_m_prime_from_hash(tbs_hash: &[u8], spec: &CompositeMlDsaSpec) -> Vec<u8> {
const PREFIX: &[u8] = b"CompositeAlgorithmSignatures2025";
let label = spec.label.as_bytes();
let mut m = Vec::with_capacity(PREFIX.len() + label.len() + 1 + tbs_hash.len());
m.extend_from_slice(PREFIX);
m.extend_from_slice(label);
m.push(0x00);
m.extend_from_slice(tbs_hash);
m
}
pub fn encode_composite_spki(
composite_oid: &[u32],
mldsa_pk: &[u8],
trad_pk: &[u8],
) -> Result<Vec<u8>, String> {
use synta::tag::{Tag, TAG_SEQUENCE};
use synta::types::string::BitStringRef;
use synta::{Encoder, Encoding, ObjectIdentifier};
let oid =
ObjectIdentifier::new(composite_oid).map_err(|e| format!("invalid composite OID: {e}"))?;
let mut raw_pk = Vec::with_capacity(mldsa_pk.len() + trad_pk.len());
raw_pk.extend_from_slice(mldsa_pk);
raw_pk.extend_from_slice(trad_pk);
let pk_bit =
BitStringRef::new(&raw_pk, 0).map_err(|e| format!("BIT STRING encoding failed: {e}"))?;
(|| -> synta::Result<Vec<u8>> {
let mut enc = Encoder::new(Encoding::Der);
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))?;
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))?;
enc.encode(&oid)?;
enc.end_constructed()?;
enc.encode(&pk_bit)?;
enc.end_constructed()?;
enc.finish()
})()
.map_err(|e| format!("SPKI DER encoding failed: {e}"))
}
pub fn encode_composite_pkcs8(
composite_oid: &[u32],
mldsa_seed: &[u8],
trad_sk: &[u8],
) -> Result<Vec<u8>, String> {
use synta::types::string::OctetStringRef;
use synta::ObjectIdentifier;
let oid =
ObjectIdentifier::new(composite_oid).map_err(|e| format!("invalid composite OID: {e}"))?;
let alg = crate::AlgorithmIdentifier {
algorithm: oid,
parameters: None,
};
let mut raw_sk = Vec::with_capacity(mldsa_seed.len() + trad_sk.len());
raw_sk.extend_from_slice(mldsa_seed);
raw_sk.extend_from_slice(trad_sk);
let pki = crate::pkcs8_types::OneAsymmetricKey {
version: synta::Integer::from_i64(0),
private_key_algorithm: alg,
private_key: OctetStringRef::new(&raw_sk),
attributes: None,
public_key: None,
};
pki.to_der()
.map_err(|e| format!("composite PKCS#8 DER encoding failed: {e}"))
}
pub fn extract_spki_bitstring_payload(spki_der: &[u8]) -> Result<Vec<u8>, String> {
use synta::types::string::BitStringRef;
use synta::{Decoder, Encoding};
let mut dec = Decoder::new(spki_der, Encoding::Der);
dec.read_tag().map_err(|e| e.to_string())?;
dec.read_length()
.and_then(|l| l.definite())
.map_err(|e| e.to_string())?;
dec.decode::<synta::Element>().map_err(|e| e.to_string())?;
let bs: BitStringRef = dec.decode().map_err(|e| e.to_string())?;
Ok(bs.as_bytes().to_vec())
}
pub fn split_composite_spki_content<'a>(
payload: &'a [u8],
spec: &CompositeMlDsaSpec,
) -> Result<(&'a [u8], &'a [u8]), String> {
if payload.len() < spec.mldsa_pk_size {
return Err(format!(
"composite SPKI payload too short for {} (mldsa_pk_size={}): got {} bytes",
spec.label,
spec.mldsa_pk_size,
payload.len()
));
}
Ok(payload.split_at(spec.mldsa_pk_size))
}
pub fn split_composite_sig<'a>(
sig: &'a [u8],
spec: &CompositeMlDsaSpec,
) -> Result<(&'a [u8], &'a [u8]), String> {
if sig.len() < spec.mldsa_sig_size {
return Err(format!(
"composite signature too short for {} (mldsa_sig_size={}): got {} bytes",
spec.label,
spec.mldsa_sig_size,
sig.len()
));
}
Ok(sig.split_at(spec.mldsa_sig_size))
}
pub fn split_composite_privkey(privkey_content: &[u8]) -> Result<(&[u8], &[u8]), String> {
const SEED_LEN: usize = 32;
if privkey_content.len() <= SEED_LEN {
return Err(format!(
"composite private key content too short: {} <= {} (no traditional key material)",
privkey_content.len(),
SEED_LEN
));
}
Ok(privkey_content.split_at(SEED_LEN))
}
pub fn pkcs8_private_key_content(pkcs8_der: &[u8]) -> Result<Vec<u8>, String> {
crate::pkcs8_types::PrivateKeyInfo::from_der(pkcs8_der)
.map(|pki| pki.private_key.as_bytes().to_vec())
.map_err(|e| format!("PKCS#8 parse error: {e}"))
}
pub fn composite_oid_components(sub_arc: u32) -> [u32; 9] {
let mut comps = [0u32; 9];
comps[..8].copy_from_slice(crate::oids::COMPOSITE_MLDSA_ARC);
comps[8] = sub_arc;
comps
}