extern crate alloc;
use alloc::vec::Vec;
use crate::error::{SigError, SigResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PreHash {
Sha256,
Sha384,
Sha512,
Sha3_256,
Sha3_384,
Sha3_512,
Shake128,
Shake256,
}
impl PreHash {
pub const fn oid_der(self) -> &'static [u8] {
match self {
PreHash::Sha256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01],
PreHash::Sha384 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02],
PreHash::Sha512 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03],
PreHash::Sha3_256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x08],
PreHash::Sha3_384 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x09],
PreHash::Sha3_512 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0A],
PreHash::Shake128 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B],
PreHash::Shake256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0C],
}
}
pub const fn digest_len(self) -> usize {
match self {
PreHash::Sha256 => 32,
PreHash::Sha384 => 48,
PreHash::Sha512 => 64,
PreHash::Sha3_256 => 32,
PreHash::Sha3_384 => 48,
PreHash::Sha3_512 => 64,
PreHash::Shake128 => 32,
PreHash::Shake256 => 64,
}
}
pub const fn collision_strength_bits(self) -> u32 {
match self {
PreHash::Sha256 => 128,
PreHash::Sha384 => 192,
PreHash::Sha512 => 256,
PreHash::Sha3_256 => 128,
PreHash::Sha3_384 => 192,
PreHash::Sha3_512 => 256,
PreHash::Shake128 => 128,
PreHash::Shake256 => 256,
}
}
pub const fn name(self) -> &'static str {
match self {
PreHash::Sha256 => "SHA-256",
PreHash::Sha384 => "SHA-384",
PreHash::Sha512 => "SHA-512",
PreHash::Sha3_256 => "SHA3-256",
PreHash::Sha3_384 => "SHA3-384",
PreHash::Sha3_512 => "SHA3-512",
PreHash::Shake128 => "SHAKE128",
PreHash::Shake256 => "SHAKE256",
}
}
}
#[cfg_attr(not(any(feature = "ml-dsa", feature = "slh-dsa")), allow(dead_code))]
pub(crate) fn frame_prehash(ctx: &[u8], hash: PreHash, digest: &[u8]) -> Vec<u8> {
let oid = hash.oid_der();
let mut out = Vec::with_capacity(2 + ctx.len() + oid.len() + digest.len());
out.push(0x01);
out.push(ctx.len() as u8);
out.extend_from_slice(ctx);
out.extend_from_slice(oid);
out.extend_from_slice(digest);
out
}
#[cfg_attr(not(any(feature = "ml-dsa", feature = "slh-dsa", feature = "fndsa")), allow(dead_code))]
pub(crate) fn check_prehash(
hash: PreHash,
digest: &[u8],
required_bits: u32,
algorithm: &'static str,
) -> SigResult<()> {
let expected = hash.digest_len();
if digest.len() != expected {
return Err(SigError::InvalidDigestLength {
hash: hash.name(),
expected,
got: digest.len(),
});
}
let strength = hash.collision_strength_bits();
if strength < required_bits {
return Err(SigError::PreHashTooWeak {
hash: hash.name(),
strength,
algorithm,
required: required_bits,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oid_der_matches_fips_204_table() {
assert_eq!(
PreHash::Sha256.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]
);
assert_eq!(
PreHash::Sha384.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02]
);
assert_eq!(
PreHash::Sha512.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]
);
assert_eq!(
PreHash::Sha3_256.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x08]
);
assert_eq!(
PreHash::Sha3_384.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x09]
);
assert_eq!(
PreHash::Sha3_512.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0A]
);
assert_eq!(
PreHash::Shake128.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B]
);
assert_eq!(
PreHash::Shake256.oid_der(),
&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0C]
);
for h in [
PreHash::Sha256, PreHash::Sha384, PreHash::Sha512,
PreHash::Sha3_256, PreHash::Sha3_384, PreHash::Sha3_512,
PreHash::Shake128, PreHash::Shake256,
] {
assert_eq!(h.oid_der().len(), 11);
assert_eq!(h.oid_der()[0], 0x06);
assert_eq!(h.oid_der()[1], 0x09);
}
}
#[test]
fn digest_len_matches_hash_output_size() {
assert_eq!(PreHash::Sha256.digest_len(), 32);
assert_eq!(PreHash::Sha384.digest_len(), 48);
assert_eq!(PreHash::Sha512.digest_len(), 64);
assert_eq!(PreHash::Sha3_256.digest_len(), 32);
assert_eq!(PreHash::Sha3_384.digest_len(), 48);
assert_eq!(PreHash::Sha3_512.digest_len(), 64);
assert_eq!(PreHash::Shake128.digest_len(), 32);
assert_eq!(PreHash::Shake256.digest_len(), 64);
}
#[test]
fn collision_strength_bits_matches_fips_table() {
assert_eq!(PreHash::Sha256.collision_strength_bits(), 128);
assert_eq!(PreHash::Sha384.collision_strength_bits(), 192);
assert_eq!(PreHash::Sha512.collision_strength_bits(), 256);
assert_eq!(PreHash::Sha3_256.collision_strength_bits(), 128);
assert_eq!(PreHash::Sha3_384.collision_strength_bits(), 192);
assert_eq!(PreHash::Sha3_512.collision_strength_bits(), 256);
assert_eq!(PreHash::Shake128.collision_strength_bits(), 128);
assert_eq!(PreHash::Shake256.collision_strength_bits(), 256);
}
#[test]
fn name_is_human_readable() {
assert_eq!(PreHash::Sha256.name(), "SHA-256");
assert_eq!(PreHash::Shake256.name(), "SHAKE256");
}
#[test]
fn frame_prehash_layout() {
let ctx = b"8gentz-agent-v1";
let digest = [0x42u8; 32];
let m_prime = frame_prehash(ctx, PreHash::Sha256, &digest);
assert_eq!(m_prime[0], 0x01, "domain byte must be 0x01 for pre-hash mode");
assert_eq!(m_prime[1], ctx.len() as u8, "second byte must be len(ctx)");
assert_eq!(&m_prime[2..2 + ctx.len()], ctx, "ctx must follow the length byte");
let oid = PreHash::Sha256.oid_der();
assert_eq!(
&m_prime[2 + ctx.len()..2 + ctx.len() + oid.len()],
oid,
"OID must follow ctx"
);
assert_eq!(
&m_prime[2 + ctx.len() + oid.len()..],
&digest[..],
"digest must follow the OID"
);
assert_eq!(m_prime.len(), 2 + ctx.len() + oid.len() + digest.len());
}
#[test]
fn frame_prehash_empty_ctx() {
let digest = [0u8; 64];
let m_prime = frame_prehash(&[], PreHash::Sha512, &digest);
assert_eq!(m_prime[0], 0x01);
assert_eq!(m_prime[1], 0);
assert_eq!(&m_prime[2..13], PreHash::Sha512.oid_der());
assert_eq!(&m_prime[13..], &digest[..]);
}
#[test]
fn check_prehash_rejects_wrong_digest_length() {
let digest = [0u8; 31]; match check_prehash(PreHash::Sha256, &digest, 128, "ML-DSA-44") {
Err(SigError::InvalidDigestLength { hash, expected, got }) => {
assert_eq!(hash, "SHA-256");
assert_eq!(expected, 32);
assert_eq!(got, 31);
}
other => panic!("expected InvalidDigestLength, got {other:?}"),
}
}
#[test]
fn check_prehash_rejects_weak_hash() {
let digest = [0u8; 32];
match check_prehash(PreHash::Sha256, &digest, 192, "ML-DSA-65") {
Err(SigError::PreHashTooWeak { hash, strength, algorithm, required }) => {
assert_eq!(hash, "SHA-256");
assert_eq!(strength, 128);
assert_eq!(algorithm, "ML-DSA-65");
assert_eq!(required, 192);
}
other => panic!("expected PreHashTooWeak, got {other:?}"),
}
}
#[test]
fn check_prehash_accepts_sufficient_strength() {
let digest = [0u8; 64];
assert!(check_prehash(PreHash::Sha512, &digest, 256, "ML-DSA-87").is_ok());
}
}