use sha2::digest::{block_api::VariableOutputCore, common::hazmat::SerializableState};
use super::super::{ProviderError, ProviderInputError};
use crate::xmldsig::DigestAlgorithm;
pub(super) fn hash(
algorithm: DigestAlgorithm,
prefixes: &[&[u8]],
tail: &[u8],
bit_len: usize,
) -> Result<zeroize::Zeroizing<[u8; 64]>, ProviderError> {
let full_octets = bit_len / 8;
let partial_bits = bit_len % 8;
let total_bits = prefixes
.iter()
.map(|bytes| bytes.len() as u128 * 8)
.sum::<u128>()
+ bit_len as u128;
macro_rules! hash {
($initial:expr, $word:ty, $words:expr, $block:expr, $length:expr, $compress:path) => {{
if $length == 8 && total_bits > u64::MAX as u128 {
return Err(ProviderError::InvalidInput(
ProviderInputError::ConcatKdfParameters,
));
}
let initial = $initial.serialize();
let mut state = zeroize::Zeroizing::new([0 as $word; $words]);
for (word, bytes) in state
.iter_mut()
.zip(initial.chunks_exact(core::mem::size_of::<$word>()))
{
*word = <$word>::from_le_bytes(bytes.try_into().expect("RustCrypto state width"));
}
let mut buffer = zeroize::Zeroizing::new([0_u8; $block]);
let mut used = 0;
for mut source in prefixes
.iter()
.copied()
.chain(core::iter::once(&tail[..full_octets]))
{
while !source.is_empty() {
if used == 0 && source.len() >= $block {
let (blocks, remainder) = source.as_chunks::<$block>();
$compress(&mut state, blocks);
source = remainder;
continue;
}
let count = source.len().min($block - used);
buffer[used..used + count].copy_from_slice(&source[..count]);
used += count;
source = &source[count..];
if used == $block {
$compress(&mut state, core::slice::from_ref(&*buffer));
used = 0;
}
}
}
buffer[used] = if partial_bits == 0 {
0x80
} else {
tail[full_octets] | (0x80 >> partial_bits)
};
used += 1;
buffer[used..].fill(0);
if used > $block - $length {
$compress(&mut state, core::slice::from_ref(&*buffer));
buffer.fill(0);
}
buffer[$block - $length..].copy_from_slice(&total_bits.to_be_bytes()[16 - $length..]);
$compress(&mut state, core::slice::from_ref(&*buffer));
let mut output = zeroize::Zeroizing::new([0; 64]);
for (bytes, word) in output
.chunks_exact_mut(core::mem::size_of::<$word>())
.zip(state.iter())
{
bytes.copy_from_slice(&word.to_be_bytes());
}
Ok(output)
}};
}
match algorithm {
DigestAlgorithm::Sha1 => hash!(
sha1::block_api::Sha1Core::default(),
u32,
5,
64,
8,
sha1::block_api::compress
),
DigestAlgorithm::Sha224 | DigestAlgorithm::Sha256 => hash!(
sha2::block_api::Sha256VarCore::new(algorithm.output_len())
.expect("SHA-2 output width"),
u32,
8,
64,
8,
sha2::block_api::compress256
),
DigestAlgorithm::Sha384 | DigestAlgorithm::Sha512 => hash!(
sha2::block_api::Sha512VarCore::new(algorithm.output_len())
.expect("SHA-2 output width"),
u64,
8,
128,
16,
sha2::block_api::compress512
),
_ => Err(ProviderError::InvalidInput(
ProviderInputError::ConcatKdfParameters,
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(value: &str) -> Vec<u8> {
let (pairs, remainder) = value.as_bytes().as_chunks::<2>();
assert!(
remainder.is_empty(),
"NIST vector must contain complete octets"
);
pairs
.iter()
.map(|pair| {
u8::from_str_radix(core::str::from_utf8(pair).expect("ASCII hex vector"), 16)
.expect("valid NIST hexadecimal octet")
})
.collect()
}
#[test]
fn bit_padding_matches_nist_shavs_one_bit_vectors() {
for (algorithm, message, expected) in [
(
DigestAlgorithm::Sha1,
0x00,
"bb6b3e18f0115b57925241676f5b1ae88747b08a",
),
(
DigestAlgorithm::Sha224,
0x80,
"0d05096bca2a4a77a2b47a05a59618d01174b37892376135c1b6e957",
),
(
DigestAlgorithm::Sha256,
0x00,
"bd4f9e98beb68c6ead3243b1b4c7fed75fa4feaab1f84795cbd8a98676a2a375",
),
(
DigestAlgorithm::Sha384,
0x00,
"634aa63038a164ae6c7d48b319f2aca0a107908e548519204c6d72dbeac0fdc3c9246674f98e8fd30221ba986e737d61",
),
(
DigestAlgorithm::Sha512,
0x00,
"b4594eb12959fc2e6979b6783554299cc0369f44083a8b0955baefd8830cda22894b0b46c0ed49490e391ad99af856cc1bd96f238c7f2a17cf37aeb7e793395a",
),
] {
let expected = hex(expected);
let actual = hash(algorithm, &[], &[message], 1).expect("valid one-bit NIST message");
assert_eq!(&actual[..algorithm.output_len()], expected, "{algorithm:?}");
}
}
#[test]
fn bit_padding_and_segment_boundaries_match_nist_shavs() {
for (algorithm, bit_len, message, expected) in [
(
DigestAlgorithm::Sha256,
447,
"86f15b8b677b7655f358a2c7fd5785bc84d31e079ed859b6af88e198debd36fccaf0ffbc785aa17a9158102aca14e6d0a362b28b54e892d2",
"eaec4af4f0632711ae6d78bcadb50eb53aee0d2e65c906cd903349750ea71c92",
),
(
DigestAlgorithm::Sha256,
448,
"3488a0d1c0998edf4c16f35f0293e1a73134bc20efc2f8f702fd241501342852b614cbe5c7be781f951c415a6574242cbe3d8de011321d26",
"75fd02880ba7d64381f6391811ec64c6852e9f579f11e47f097438089d7ed905",
),
(
DigestAlgorithm::Sha512,
895,
"ea08fbebb5bce55e0be90e187df11db48a63355b09d07738c7dbc92b7090d6c2b3f75665a1e890d056201dc7dc95ee044c07917536d8afa4431dda7719f585197f1a5438c2b5977157771108c1e5fdddb6f3539b7dd547339a121289addaf66c98997ccce0cc012756147045f6885124",
"be830cfd7a9ac5c23fff9401344bc95be5c3adaf528ecfa21a1d7bd5e230ad8fb665f801a91be6cca863f54e05f83184efcc3517c019f24868cb2fdcffa7fcc8",
),
(
DigestAlgorithm::Sha512,
896,
"d3ddddf805b1678a02e39200f6440047acbb062e4a2f046a3ca7f1dd6eb03a18be00cd1eb158706a64af5834c68cf7f105b415194605222c99a2cbf72c50cb14bf2004b0574fce90376a8f560cb58c0168cefeb4718e69b8db3029c313b54d7bbd86a936615c2704615d5eef2f886681",
"c614e385837ecd4b1e471edf2143cfb8fe2697c5a3ee3ad56c13cff09733ccf4ce3b993de7fb726e89b2283629ec48a0e6fbe6b50003c378a0f5b0405a629619",
),
] {
let message = hex(message);
let expected = hex(expected);
for split in [0, 1, 31, 32, 55] {
let actual = hash(
algorithm,
&[&message[..split]],
&message[split..],
bit_len - split * 8,
)
.expect("valid split NIST message");
assert_eq!(
&actual[..algorithm.output_len()],
expected,
"{algorithm:?}/{bit_len}/{split}"
);
}
}
}
}