proto: sha1, for the WebSocket handshake check
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -9,6 +9,7 @@ pub mod hash;
|
||||
pub mod hosts;
|
||||
pub mod ids;
|
||||
pub mod log;
|
||||
pub mod sha1;
|
||||
pub mod tools;
|
||||
pub mod wire;
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! SHA-1 (FIPS 180-4), used only to check the `Sec-WebSocket-Accept` header of a WebSocket
|
||||
//! handshake (RFC 6455, section 4.2.2). Never use it for anything that needs to resist attack.
|
||||
|
||||
/// The digest of `data`.
|
||||
pub fn sha1(data: &[u8]) -> [u8; 20] {
|
||||
let mut h = Sha1::new();
|
||||
h.update(data);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// SHA-1 fed in pieces.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sha1 {
|
||||
state: [u32; 5],
|
||||
block: [u8; 64],
|
||||
filled: usize,
|
||||
length: u64,
|
||||
}
|
||||
|
||||
impl Default for Sha1 {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sha1 {
|
||||
pub fn new() -> Sha1 {
|
||||
Sha1 {
|
||||
state: [
|
||||
0x6745_2301,
|
||||
0xEFCD_AB89,
|
||||
0x98BA_DCFE,
|
||||
0x1032_5476,
|
||||
0xC3D2_E1F0,
|
||||
],
|
||||
block: [0; 64],
|
||||
filled: 0,
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, mut data: &[u8]) {
|
||||
// Add 8 * data.len() to `length` (wrapping; `u64::try_from`, never `as`). Copy bytes into
|
||||
// `block` from `filled` on; each time it is full (64), `compress` it and set `filled` to 0.
|
||||
// Use `split_at` and `get_mut(..)`, no indexing that can go out of bounds.
|
||||
self.length = self
|
||||
.length
|
||||
.wrapping_add(8u64.wrapping_mul(data.len() as u64));
|
||||
while !data.is_empty() {
|
||||
let space = 64 - self.filled;
|
||||
let take = data.len().min(space);
|
||||
let (left, right) = data.split_at(take);
|
||||
if let Some(dst) = self.block.get_mut(self.filled..self.filled + take) {
|
||||
dst.copy_from_slice(left);
|
||||
}
|
||||
self.filled += take;
|
||||
if self.filled == 64 {
|
||||
let block = self.block;
|
||||
self.compress(&block);
|
||||
self.filled = 0;
|
||||
}
|
||||
data = right;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> [u8; 20] {
|
||||
// Save `length`. Feed 0x80 then zeros so that 56 bytes of the block are filled (56 -
|
||||
// filled, or 120 - filled when filled >= 56), then the saved length as 8 big-endian bytes,
|
||||
// through `update`. `update` adds to `length`: put the saved value back after. Then the
|
||||
// five state words, big-endian.
|
||||
let saved = self.length;
|
||||
let zeros = if self.filled < 56 {
|
||||
56 - self.filled
|
||||
} else {
|
||||
120 - self.filled
|
||||
};
|
||||
let total = zeros + 8;
|
||||
let mut pad = [0u8; 128];
|
||||
pad[0] = 0x80;
|
||||
pad[zeros..total].copy_from_slice(&saved.to_be_bytes());
|
||||
self.update(&pad[..total]);
|
||||
self.length = saved;
|
||||
|
||||
let mut out = [0u8; 20];
|
||||
for (i, word) in self.state.iter().enumerate() {
|
||||
out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn compress(&mut self, block: &[u8; 64]) {
|
||||
// FIPS 180-4, section 6.1.2: w[0..16] are the block as big-endian u32s; w[i] = (w[i-3] ^
|
||||
// w[i-8] ^ w[i-14] ^ w[i-16]).rotate_left(1) for 16..80. Eighty rounds with f and k by
|
||||
// range: 0..=19 (b & c) | (!b & d), 0x5A827999; 20..=39 b ^ c ^ d, 0x6ED9EBA1; 40..=59 (b &
|
||||
// c) | (b & d) | (c & d), 0x8F1BBCDC; 60..=79 b ^ c ^ d, 0xCA62C1D6. All additions
|
||||
// wrapping. Add a..e into state.
|
||||
let mut w: [u32; 80] = [0; 80];
|
||||
let (chunks, _) = block.as_chunks::<4>();
|
||||
for (i, chunk) in chunks.iter().enumerate() {
|
||||
w[i] = u32::from_be_bytes(*chunk);
|
||||
}
|
||||
for i in 16..80 {
|
||||
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
|
||||
}
|
||||
|
||||
let (mut a, mut b, mut c, mut d, mut e) = (
|
||||
self.state[0],
|
||||
self.state[1],
|
||||
self.state[2],
|
||||
self.state[3],
|
||||
self.state[4],
|
||||
);
|
||||
for (round, &ww) in w.iter().enumerate() {
|
||||
let (f, k) = if round <= 19 {
|
||||
((b & c) | (!b & d), 0x5A82_7999)
|
||||
} else if round <= 39 {
|
||||
(b ^ c ^ d, 0x6ED9_EBA1)
|
||||
} else if round <= 59 {
|
||||
((b & c) | (b & d) | (c & d), 0x8F1B_BCDC)
|
||||
} else {
|
||||
(b ^ c ^ d, 0xCA62_C1D6)
|
||||
};
|
||||
let temp = a
|
||||
.rotate_left(5)
|
||||
.wrapping_add(f)
|
||||
.wrapping_add(e)
|
||||
.wrapping_add(k);
|
||||
e = d;
|
||||
d = c;
|
||||
c = b.rotate_left(30);
|
||||
b = a;
|
||||
a = temp.wrapping_add(ww);
|
||||
}
|
||||
|
||||
self.state[0] = self.state[0].wrapping_add(a);
|
||||
self.state[1] = self.state[1].wrapping_add(b);
|
||||
self.state[2] = self.state[2].wrapping_add(c);
|
||||
self.state[3] = self.state[3].wrapping_add(d);
|
||||
self.state[4] = self.state[4].wrapping_add(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! SHA-1 against FIPS 180 and RFC 3174 vectors, and against `sha1sum` for the lengths around the
|
||||
//! 64-byte block where padding changes shape. Every case is also fed in pieces. Do not edit.
|
||||
|
||||
use proto::sha1::{Sha1, sha1};
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn check(data: &[u8], want: &str) {
|
||||
assert_eq!(hex(&sha1(data)), want, "whole, {} bytes", data.len());
|
||||
for split in [
|
||||
0,
|
||||
1,
|
||||
data.len() / 2,
|
||||
data.len().saturating_sub(1),
|
||||
data.len(),
|
||||
] {
|
||||
let split = split.min(data.len());
|
||||
let mut h = Sha1::new();
|
||||
h.update(&data[..split]);
|
||||
h.update(&data[split..]);
|
||||
assert_eq!(hex(&h.finish()), want, "split at {split} of {}", data.len());
|
||||
}
|
||||
let mut h = Sha1::new();
|
||||
for b in data {
|
||||
h.update(&[*b]);
|
||||
}
|
||||
assert_eq!(hex(&h.finish()), want, "byte by byte, {} bytes", data.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_vectors() {
|
||||
check(b"", "da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
||||
check(b"abc", "a9993e364706816aba3e25717850c26c9cd0d89d");
|
||||
check(
|
||||
b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
|
||||
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
|
||||
);
|
||||
check(
|
||||
b"The quick brown fox jumps over the lazy dog",
|
||||
"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_million_a() {
|
||||
let data = vec![b'a'; 1_000_000];
|
||||
assert_eq!(
|
||||
hex(&sha1(&data)),
|
||||
"34aa973cd4c4daa4f61eeb2bdbad27316534016f"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lengths_around_the_block_boundary() {
|
||||
// `printf 'a%.0s' $(seq N) | sha1sum`, N = 55, 56, 63, 64, 65, 119, 120.
|
||||
let cases = [
|
||||
(55, "c1c8bbdc22796e28c0e15163d20899b65621d65a"),
|
||||
(56, "c2db330f6083854c99d4b5bfb6e8f29f201be699"),
|
||||
(63, "03f09f5b158a7a8cdad920bddc29b81c18a551f5"),
|
||||
(64, "0098ba824b5c16427bd7a1122a5a442a25ec644d"),
|
||||
(65, "11655326c708d70319be2610e8a57d9a5b959d3b"),
|
||||
(119, "ee971065aaa017e0632a8ca6c77bb3bf8b1dfc56"),
|
||||
(120, "f34c1488385346a55709ba056ddd08280dd4c6d6"),
|
||||
];
|
||||
for (n, want) in cases {
|
||||
check(&vec![b'a'; n], want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_websocket_handshake_example() {
|
||||
// RFC 6455, section 1.3: the key and the GUID give this digest (its base64 is
|
||||
// "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=").
|
||||
let digest = sha1(b"dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
assert_eq!(hex(&digest), "b37a4f2cc0624f1690f64606cf385945b2bec4ea");
|
||||
}
|
||||
Reference in New Issue
Block a user