Skip to content

Commit c54bf37

Browse files
committed
Add basic SHA1 support
1 parent 977f10a commit c54bf37

5 files changed

Lines changed: 294 additions & 2 deletions

File tree

graviola/src/high/hash.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
use core::ops::{Deref, DerefMut};
55

66
use crate::low::ct_equal;
7+
use crate::mid::sha1;
78
use crate::mid::sha2;
89

910
/// Output from a hash function.
1011
///
1112
/// This has one variant per supported hash function.
1213
#[derive(Clone, Debug)]
1314
pub enum HashOutput {
15+
/// Output from SHA1
16+
Sha1([u8; sha1::Sha1Context::OUTPUT_SZ]),
1417
/// Output from SHA256
1518
Sha256([u8; sha2::Sha256Context::OUTPUT_SZ]),
1619
/// Output from SHA384
@@ -43,6 +46,7 @@ impl HashOutput {
4346
impl PartialEq for HashOutput {
4447
fn eq(&self, other: &Self) -> bool {
4548
match (self, other) {
49+
(Self::Sha1(s), Self::Sha1(o)) => ct_equal(s, o),
4650
(Self::Sha256(s), Self::Sha256(o)) => ct_equal(s, o),
4751
(Self::Sha384(s), Self::Sha384(o)) => ct_equal(s, o),
4852
(Self::Sha512(s), Self::Sha512(o)) => ct_equal(s, o),
@@ -54,6 +58,7 @@ impl PartialEq for HashOutput {
5458
impl AsRef<[u8]> for HashOutput {
5559
fn as_ref(&self) -> &[u8] {
5660
match self {
61+
Self::Sha1(v) => v,
5762
Self::Sha256(v) => v,
5863
Self::Sha384(v) => v,
5964
Self::Sha512(v) => v,
@@ -64,6 +69,7 @@ impl AsRef<[u8]> for HashOutput {
6469
impl AsMut<[u8]> for HashOutput {
6570
fn as_mut(&mut self) -> &mut [u8] {
6671
match self {
72+
Self::Sha1(v) => v,
6773
Self::Sha256(v) => v,
6874
Self::Sha384(v) => v,
6975
Self::Sha512(v) => v,
@@ -259,6 +265,49 @@ impl HashContext for Sha512Context {
259265
}
260266
}
261267

268+
/// This is SHA1.
269+
///
270+
/// Do not use SHA1 for new applications or for applications involving signatures.
271+
///
272+
/// This is described in [FIPS180-1](https://nvlpubs.nist.gov/nistpubs/Legacy/FIPS/fipspub180-1.pdf).
273+
#[derive(Clone)]
274+
pub struct Sha1;
275+
276+
impl Hash for Sha1 {
277+
type Context = Sha1Context;
278+
279+
fn new() -> Self::Context {
280+
Sha1Context(sha1::Sha1Context::new())
281+
}
282+
283+
fn hash(bytes: &[u8]) -> HashOutput {
284+
let mut ctx = Self::new();
285+
ctx.update(bytes);
286+
ctx.finish()
287+
}
288+
289+
fn zeroed_block() -> HashBlock {
290+
HashBlock::new(sha1::Sha1Context::BLOCK_SZ)
291+
}
292+
293+
fn zeroed_output() -> HashOutput {
294+
HashOutput::Sha1([0u8; sha1::Sha1Context::OUTPUT_SZ])
295+
}
296+
}
297+
298+
#[derive(Clone)]
299+
pub struct Sha1Context(sha1::Sha1Context);
300+
301+
impl HashContext for Sha1Context {
302+
fn update(&mut self, bytes: &[u8]) {
303+
self.0.update(bytes)
304+
}
305+
306+
fn finish(self) -> HashOutput {
307+
HashOutput::Sha1(self.0.finish())
308+
}
309+
}
310+
262311
#[cfg(test)]
263312
#[cfg_attr(coverage_nightly, coverage(off))]
264313
mod tests {

graviola/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ pub mod signing {
166166

167167
/// Cryptographic hash functions.
168168
pub mod hashing {
169-
pub use super::high::hash::{Hash, HashContext, HashOutput, Sha256, Sha384, Sha512};
169+
pub use super::high::hash::{Hash, HashContext, HashOutput, Sha1, Sha256, Sha384, Sha512};
170170
pub use super::high::hmac;
171+
pub use super::mid::sha1;
171172
pub use super::mid::sha2;
172173
pub use super::mid::sha3;
173174
}

graviola/src/mid/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(super) mod p384;
1212
pub(super) mod rng;
1313
pub(super) mod rsa_priv;
1414
pub(super) mod rsa_pub;
15+
pub mod sha1;
1516
pub mod sha2;
1617
pub mod sha3;
1718
pub(super) mod util;

graviola/src/mid/sha1.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Written for Graviola by Joe Birr-Pixton, 2025.
2+
// Based on 2014 version written for cifra.
3+
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0
4+
5+
//! The SHA1 hash function.
6+
//!
7+
//! Do not use SHA1 for new applications or for applications involving signatures.
8+
//!
9+
//! This is described in [FIPS180-1](https://nvlpubs.nist.gov/nistpubs/Legacy/FIPS/fipspub180-1.pdf).
10+
11+
use crate::low::Blockwise;
12+
13+
/// A context for incremental computation of SHA1.
14+
#[derive(Clone)]
15+
pub struct Sha1Context {
16+
h: [u32; 5],
17+
blockwise: Blockwise<{ Self::BLOCK_SZ }>,
18+
nblocks: usize,
19+
}
20+
21+
impl Sha1Context {
22+
/// Start a new SHA1 hash computation.
23+
pub const fn new() -> Self {
24+
Self {
25+
h: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
26+
blockwise: Blockwise::new(),
27+
nblocks: 0,
28+
}
29+
}
30+
31+
/// Add `bytes` to the ongoing hash computation.
32+
pub fn update(&mut self, bytes: &[u8]) {
33+
if self.blockwise.used() == 0 && bytes.len().is_multiple_of(Self::BLOCK_SZ) {
34+
self.update_blocks(bytes);
35+
return;
36+
}
37+
38+
let bytes = self.blockwise.add_leading(bytes);
39+
40+
if let Some(block) = self.blockwise.take() {
41+
self.update_blocks(&block);
42+
}
43+
44+
let (whole_blocks, remainder) = {
45+
let whole_len = bytes.len() - (bytes.len() & (Self::BLOCK_SZ - 1));
46+
(&bytes[..whole_len], &bytes[whole_len..])
47+
};
48+
49+
self.update_blocks(whole_blocks);
50+
51+
self.blockwise.add_trailing(remainder);
52+
}
53+
54+
/// Complete the SHA1 computation, returning the hash output.
55+
pub fn finish(mut self) -> [u8; Self::OUTPUT_SZ] {
56+
let bytes = self
57+
.nblocks
58+
.checked_mul(Self::BLOCK_SZ)
59+
.and_then(|bytes| bytes.checked_add(self.blockwise.used()))
60+
.unwrap();
61+
62+
let bits = bytes
63+
.checked_mul(8)
64+
.expect("excess data processed by hash function");
65+
66+
let last_blocks = self
67+
.blockwise
68+
.md_pad_with_length(&(bits as u64).to_be_bytes());
69+
self.update_blocks(last_blocks.as_ref());
70+
71+
let mut r = [0u8; Self::OUTPUT_SZ];
72+
for (out, state) in r.chunks_exact_mut(4).zip(self.h.iter()) {
73+
out.copy_from_slice(&state.to_be_bytes());
74+
}
75+
r
76+
}
77+
78+
fn update_blocks(&mut self, blocks: &[u8]) {
79+
debug_assert!(blocks.len().is_multiple_of(Self::BLOCK_SZ));
80+
if !blocks.is_empty() {
81+
sha1_compress_blocks(&mut self.h, blocks);
82+
self.nblocks = self.nblocks.saturating_add(blocks.len() / Self::BLOCK_SZ);
83+
}
84+
}
85+
86+
/// The internal block size of SHA1.
87+
pub const BLOCK_SZ: usize = 64;
88+
89+
/// The output size of SHA1.
90+
pub const OUTPUT_SZ: usize = 20;
91+
}
92+
93+
fn sha1_compress_block(state: &mut [u32; 5], block: &[u8]) {
94+
let mut a = state[0];
95+
let mut b = state[1];
96+
let mut c = state[2];
97+
let mut d = state[3];
98+
let mut e = state[4];
99+
100+
// This is a 16-word window into the whole W array.
101+
let mut w: [u32; 16] = [0; 16];
102+
103+
for t in 0..80 {
104+
// For W[0..16] we process the input into W.
105+
// For W[16..80] we compute the next W value:
106+
//
107+
// W[t] = (W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]) <<< 1
108+
//
109+
// But all W indices are reduced mod 16 into our window.
110+
let w_t = if t < 16 {
111+
let w_t = u32::from_be_bytes(block[t * 4..(t + 1) * 4].try_into().unwrap());
112+
w[t] = w_t;
113+
w_t
114+
} else {
115+
let w_t = (w[(t - 3) % 16] ^ w[(t - 8) % 16] ^ w[(t - 14) % 16] ^ w[(t - 16) % 16])
116+
.rotate_left(1);
117+
w[t % 16] = w_t;
118+
w_t
119+
};
120+
121+
let (f, k) = match t {
122+
0..20 => ((b & c) | (!b & d), 0x5a827999),
123+
20..40 => (b ^ c ^ d, 0x6ed9eba1),
124+
40..60 => ((b & c) | (b & d) | (c & d), 0x8f1bbcdc),
125+
_ => (b ^ c ^ d, 0xca62c1d6),
126+
};
127+
128+
let temp = a
129+
.rotate_left(5)
130+
.wrapping_add(f)
131+
.wrapping_add(e)
132+
.wrapping_add(k)
133+
.wrapping_add(w_t);
134+
e = d;
135+
d = c;
136+
c = b.rotate_left(30);
137+
b = a;
138+
a = temp;
139+
}
140+
141+
state[0] = state[0].wrapping_add(a);
142+
state[1] = state[1].wrapping_add(b);
143+
state[2] = state[2].wrapping_add(c);
144+
state[3] = state[3].wrapping_add(d);
145+
state[4] = state[4].wrapping_add(e);
146+
}
147+
148+
pub(crate) fn sha1_compress_blocks(state: &mut [u32; 5], blocks: &[u8]) {
149+
debug_assert!(blocks.len().is_multiple_of(64));
150+
151+
for block in blocks.chunks_exact(64) {
152+
sha1_compress_block(state, block);
153+
}
154+
}
155+
156+
#[cfg(test)]
157+
#[cfg_attr(coverage_nightly, coverage(off))]
158+
mod tests {
159+
use super::*;
160+
161+
#[test]
162+
fn test_vectors() {
163+
vector(
164+
b"",
165+
b"\xda\x39\xa3\xee\x5e\x6b\x4b\x0d\x32\x55\xbf\xef\x95\x60\x18\x90\xaf\xd8\x07\x09",
166+
);
167+
vector(
168+
b"abc",
169+
b"\xa9\x99\x3e\x36\x47\x06\x81\x6a\xba\x3e\x25\x71\x78\x50\xc2\x6c\x9c\xd0\xd8\x9d",
170+
);
171+
vector(
172+
b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
173+
b"\x84\x98\x3e\x44\x1c\x3b\xd2\x6e\xba\xae\x4a\xa1\xf9\x51\x29\xe5\xe5\x46\x70\xf1",
174+
);
175+
vector(
176+
b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
177+
b"\xa4\x9b\x24\x46\xa0\x2c\x64\x5b\xf4\x19\xf9\x95\xb6\x70\x91\x25\x3a\x04\xa2\x59",
178+
);
179+
180+
fn vector(message: &[u8], expected: &[u8; Sha1Context::OUTPUT_SZ]) {
181+
let mut ctx = Sha1Context::new();
182+
ctx.update(message);
183+
assert_eq!(&ctx.finish(), expected);
184+
}
185+
}
186+
187+
#[test]
188+
fn sha1_all_lengths() {
189+
// see cifra `vector_length` and associated
190+
let mut outer = Sha1Context::new();
191+
192+
for len in 0..1024 {
193+
let mut inner = Sha1Context::new();
194+
195+
for _ in 0..len {
196+
inner.update(&[len as u8]);
197+
}
198+
199+
outer.update(&inner.finish());
200+
}
201+
202+
assert_eq!(
203+
&outer.finish(),
204+
b"\x15\x53\x65\xcf\x77\xee\xd4\x8f\x46\xe2\x55\xc7\xdd\xdf\xfd\x0a\xf6\x99\x88\xbe"
205+
);
206+
}
207+
}

graviola/tests/wycheproof.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::io::BufReader;
44
use graviola::Error;
55
use graviola::aead::{AesGcm, ChaCha20Poly1305, XChaCha20Poly1305};
66
use graviola::hashing::hmac::Hmac;
7-
use graviola::hashing::{Sha256, Sha384, Sha512};
7+
use graviola::hashing::{Sha1, Sha256, Sha384, Sha512};
88
use graviola::key_agreement::{mlkem768, p256, p384, x25519};
99
use graviola::signing::{ecdsa, eddsa, rsa};
1010
use serde::Deserialize;
@@ -151,6 +151,40 @@ impl Drop for Summary {
151151
}
152152
}
153153

154+
#[test]
155+
fn hmac_sha1_tests() {
156+
let data_file = File::open("../thirdparty/wycheproof/testvectors_v1/hmac_sha1_test.json")
157+
.expect("failed to open data file");
158+
159+
let reader = BufReader::new(data_file);
160+
let tests: TestFile = serde_json::from_reader(reader).expect("invalid test JSON");
161+
let mut summary = Summary::new();
162+
163+
for group in tests.groups {
164+
summary.group(&group);
165+
for test in group.tests {
166+
summary.start(&test);
167+
168+
let mut ctx = Hmac::<Sha1>::new(test.key);
169+
ctx.update(test.msg);
170+
let result = match test.tag.len() {
171+
20 => ctx.verify(&test.tag),
172+
10 => match ctx.finish().truncated_ct_equal::<10>(&test.tag) {
173+
true => Ok(()),
174+
false => Err(Error::BadSignature),
175+
},
176+
other => todo!("unhandled truncated hmac {other:?}"),
177+
};
178+
179+
match (test.result, result) {
180+
(ExpectedResult::Valid, Ok(())) => {}
181+
(ExpectedResult::Invalid, Err(Error::BadSignature)) => {}
182+
_ => panic!("expected {:?} got {:?}", test.result, result),
183+
}
184+
}
185+
}
186+
}
187+
154188
#[test]
155189
fn hmac_sha256_tests() {
156190
let data_file = File::open("../thirdparty/wycheproof/testvectors_v1/hmac_sha256_test.json")

0 commit comments

Comments
 (0)