Skip to main content

keccak_rhdl/
sponge.rs

1//! Legacy Keccak-256 rate-block packing and absorption.
2
3use rhdl::prelude::*;
4
5use crate::{KeccakState, keccak_f1600, software_keccak_f1600};
6
7/// Keccak-256's byte rate (`1600 - 2*256` bits).
8pub const RATE_BYTES: usize = 136;
9
10/// Keccak-256's rate expressed as 64-bit lanes.
11pub const RATE_LANES: usize = RATE_BYTES / 8;
12
13/// Domain byte used by legacy Keccak padding.
14///
15/// SHA3-256 uses `0x06`; using it here is a compatibility bug.
16pub const LEGACY_DOMAIN_BYTE: u8 = 0x01;
17
18/// A 256-bit Keccak digest in byte order.
19pub type Digest = [u8; 32];
20
21/// One 1,088-bit Keccak-256 absorption block.
22///
23/// `lanes[0]` holds bytes 0 through 7 in little-endian order.  The block must
24/// already include legacy `pad10*1` finalization when it is the last block.
25#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Digital)]
26pub struct RateBlock {
27    /// Seventeen little-endian 64-bit lanes.
28    pub lanes: [b64; RATE_LANES],
29}
30
31/// XOR one complete rate block into the 17 rate lanes of a state.
32///
33/// Keeping this wiring as a separately testable kernel proves the flattened
34/// [`RateBlock`] lane contract without requiring an external simulator to
35/// compile a deliberately deep 24-round combinational permutation.
36#[kernel]
37pub fn xor_rate_block(mut state: KeccakState, block: RateBlock) -> KeccakState {
38    state[0] ^= block.lanes[0];
39    state[1] ^= block.lanes[1];
40    state[2] ^= block.lanes[2];
41    state[3] ^= block.lanes[3];
42    state[4] ^= block.lanes[4];
43    state[5] ^= block.lanes[5];
44    state[6] ^= block.lanes[6];
45    state[7] ^= block.lanes[7];
46    state[8] ^= block.lanes[8];
47    state[9] ^= block.lanes[9];
48    state[10] ^= block.lanes[10];
49    state[11] ^= block.lanes[11];
50    state[12] ^= block.lanes[12];
51    state[13] ^= block.lanes[13];
52    state[14] ^= block.lanes[14];
53    state[15] ^= block.lanes[15];
54    state[16] ^= block.lanes[16];
55    state
56}
57
58/// XOR one complete rate block into a state and permute it.
59///
60/// A downstream scheduler can feed the result into the next block absorption.
61/// The capacity lanes (17 through 24) are preserved before the permutation.
62#[kernel]
63pub fn absorb_rate_block(state: KeccakState, block: RateBlock) -> KeccakState {
64    keccak_f1600(xor_rate_block(state, block))
65}
66
67/// Extract the first 256 rate bits as a Keccak digest.
68///
69/// This is a host-side conversion utility.  Bytes within each state lane are
70/// serialized little-endian.
71#[must_use]
72pub fn digest_from_state(state: KeccakState) -> Digest {
73    let mut digest = [0_u8; 32];
74    for lane in 0..4 {
75        let bytes = lane_as_u64(state[lane]).to_le_bytes();
76        digest[lane * 8..lane * 8 + 8].copy_from_slice(&bytes);
77    }
78    digest
79}
80
81/// Host-side legacy Keccak-256 sponge and block-packing model.
82///
83/// The model accepts arbitrary byte slices, but it is especially intended to
84/// prepare the fixed 32-, 35-, 64-, and 2,144-byte messages used by `HashSigsRS`.
85#[derive(Debug, Default, Copy, Clone)]
86pub struct LegacyKeccak256;
87
88impl LegacyKeccak256 {
89    /// Pack a message into fully padded 136-byte rate blocks.
90    ///
91    /// The returned vector always contains at least one block.  A message whose
92    /// length is exactly divisible by 136 receives an additional padding block.
93    #[must_use]
94    pub fn padded_blocks(message: &[u8]) -> Vec<RateBlock> {
95        let full_blocks = message.len() / RATE_BYTES;
96        let remainder = message.len() % RATE_BYTES;
97        let mut blocks = Vec::with_capacity(full_blocks + 1);
98
99        for chunk in message[..full_blocks * RATE_BYTES].chunks_exact(RATE_BYTES) {
100            blocks.push(Self::pack_rate_bytes(chunk));
101        }
102
103        let mut tail = [0_u8; RATE_BYTES];
104        tail[..remainder].copy_from_slice(&message[full_blocks * RATE_BYTES..]);
105        tail[remainder] ^= LEGACY_DOMAIN_BYTE;
106        tail[RATE_BYTES - 1] ^= 0x80;
107        blocks.push(Self::pack_rate_bytes(&tail));
108        blocks
109    }
110
111    /// Hash a byte slice with legacy Keccak-256.
112    #[must_use]
113    pub fn digest(message: &[u8]) -> Digest {
114        let mut state = [0_u64; 25];
115        for block in Self::padded_blocks(message) {
116            for (state_lane, block_lane) in state.iter_mut().zip(block.lanes) {
117                *state_lane ^= lane_as_u64(block_lane);
118            }
119            state = software_keccak_f1600(state);
120        }
121        let mut digest = [0_u8; 32];
122        for lane in 0..4 {
123            digest[lane * 8..lane * 8 + 8].copy_from_slice(&state[lane].to_le_bytes());
124        }
125        digest
126    }
127
128    fn pack_rate_bytes(bytes: &[u8]) -> RateBlock {
129        debug_assert_eq!(bytes.len(), RATE_BYTES);
130        let mut block = RateBlock::default();
131        for (lane, chunk) in bytes.chunks_exact(8).enumerate() {
132            let mut lane_bytes = [0_u8; 8];
133            lane_bytes.copy_from_slice(chunk);
134            block.lanes[lane] = b64(u64::from_le_bytes(lane_bytes).into());
135        }
136        block
137    }
138}
139
140// `b64` is statically bounded to 64 bits, so this narrowing conversion cannot
141// discard a set bit.  Keeping it here makes every host/hardware boundary audit
142// use the same explicit conversion.
143#[allow(clippy::cast_possible_truncation)]
144const fn lane_as_u64(lane: b64) -> u64 {
145    lane.raw() as u64
146}