Skip to main content

wots_rhdl/
blocks.rs

1//! Fixed SHA-256 block builders for the performance-profile WOTS datapath.
2//!
3//! Every builder returns sixteen already-decoded, big-endian SHA-256 words.
4//! There is no byte-order conversion left for a compression lane to infer.
5//! The builders are deliberately fixed-size: accepting an arbitrary byte
6//! stream in hardware would spend area on length and padding machinery that
7//! the `HASHSIGS_SHA256_GENERIC_V1` protocol never exercises.
8
9use rhdl::prelude::*;
10
11/// Bytes in one SHA-256 digest or WOTS segment.
12pub const HASH_BYTES: usize = 32;
13
14/// Bytes in one SHA-256 compression block.
15pub const SHA_BLOCK_BYTES: usize = 64;
16
17/// Bit length of the 32-byte fixed inputs.
18pub const HASH_INPUT_BITS: u32 = 256;
19
20/// Bit length of `0x03 || seed || u16_be(index)`.
21pub const PRF_INPUT_BITS: u32 = 280;
22
23/// Bit length of a 64-byte secret-segment preimage.
24pub const SECRET_INPUT_BITS: u32 = 512;
25
26/// Bytes in the concatenated 67 public-key endpoints.
27pub const ENDPOINT_STREAM_BYTES: usize = 2_144;
28
29/// Bit length of the concatenated 67 public-key endpoints.
30pub const ENDPOINT_STREAM_BITS: u32 = 17_152;
31
32/// Byte-oriented digest input used at WOTS context-storage boundaries.
33pub type HashBytes = [b8; HASH_BYTES];
34
35/// Big-endian words presented to a SHA-256 compression lane.
36pub type ShaBlock = [b32; 16];
37
38/// Eight big-endian SHA-256 chaining or digest words.
39pub type HashWords = [b32; 8];
40
41/// Input to the domain-separated PRF block builder.
42#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
43pub struct PrfBlockInput {
44    /// PRF key material.
45    pub seed: HashBytes,
46    /// Big-endian PRF index. Segment indices use `1..=67`.
47    pub index: b16,
48}
49
50/// Input to the 64-byte secret-segment first-block builder.
51#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
52pub struct SecretDataInput {
53    /// Function key, equal to randomization mask zero.
54    pub function_key: HashBytes,
55    /// Domain-separated per-segment PRF result.
56    pub secret_prf: HashBytes,
57}
58
59/// Input to one randomized WOTS chain transition.
60#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
61pub struct ChainBlockInput {
62    /// Current chain state.
63    pub state: HashBytes,
64    /// Mask selected by the one-based chain step.
65    pub mask: HashBytes,
66}
67
68/// Two adjacent 32-byte endpoints forming an ordinary public-key hash block.
69#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
70pub struct EndpointPairInput {
71    /// Endpoint at even segment index `2 * block_index`.
72    pub first: HashBytes,
73    /// Endpoint at odd segment index `2 * block_index + 1`.
74    pub second: HashBytes,
75}
76
77/// Convert 64 explicitly ordered bytes to sixteen big-endian words.
78#[allow(clippy::needless_range_loop)] // RHDL kernels require indexable fixed arrays.
79#[kernel]
80pub fn bytes_to_words_be_kernel(bytes: [b8; SHA_BLOCK_BYTES]) -> ShaBlock {
81    let mut words = [b32(0); 16];
82    for word in 0..16 {
83        let byte = word * 4;
84        let byte_zero: b32 = bytes[byte].resize();
85        let byte_one: b32 = bytes[byte + 1].resize();
86        let byte_two: b32 = bytes[byte + 2].resize();
87        let byte_three: b32 = bytes[byte + 3].resize();
88        words[word] = (byte_zero << 24) | (byte_one << 16) | (byte_two << 8) | byte_three;
89    }
90    words
91}
92
93/// Convert eight SHA-256 words to their canonical 32-byte representation.
94///
95/// Context memories retain hashes as words because that is the compression
96/// lane's native interface. Fixed-preimage builders operate on bytes, so this
97/// conversion is the single, explicit byte-order boundary between them.
98#[kernel]
99pub fn hash_words_to_bytes_kernel(words: HashWords) -> HashBytes {
100    let mut bytes = [b8(0); HASH_BYTES];
101    for word in 0..8 {
102        bytes[word * 4] = (words[word] >> 24).resize();
103        bytes[word * 4 + 1] = (words[word] >> 16).resize();
104        bytes[word * 4 + 2] = (words[word] >> 8).resize();
105        bytes[word * 4 + 3] = words[word].resize();
106    }
107    bytes
108}
109
110/// Return the FIPS 180-4 SHA-256 initial chaining value in RHDL widths.
111///
112/// The dummy input keeps this usable as an ordinary RHDL kernel; its value is
113/// intentionally ignored.
114#[kernel]
115#[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
116pub fn initial_state_words_kernel(_dummy: b1) -> HashWords {
117    [
118        b32(0x6a09_e667),
119        b32(0xbb67_ae85),
120        b32(0x3c6e_f372),
121        b32(0xa54f_f53a),
122        b32(0x510e_527f),
123        b32(0x9b05_688c),
124        b32(0x1f83_d9ab),
125        b32(0x5be0_cd19),
126    ]
127}
128
129/// Build the sole padded block for an arbitrary protocol-owned 32-byte value.
130#[kernel]
131pub fn sha256_32_byte_block_kernel(input: HashBytes) -> ShaBlock {
132    let mut bytes = [b8(0); SHA_BLOCK_BYTES];
133    for index in 0..HASH_BYTES {
134        bytes[index] = input[index];
135    }
136    bytes[HASH_BYTES] = b8(0x80);
137    bytes[62] = b8(0x01);
138    bytes[63] = b8(0x00);
139    bytes_to_words_be_kernel(bytes)
140}
141
142/// Build the padded block that hashes a caller-supplied private seed.
143///
144/// This distinct kernel keeps the private-seed ingress visible in generated
145/// hierarchy and prevents it from being confused with an intermediate hash.
146#[kernel]
147pub fn private_seed_block_kernel(private_seed: HashBytes) -> ShaBlock {
148    sha256_32_byte_block_kernel(private_seed)
149}
150
151/// Build `0x03 || seed || u16_be(index)` plus SHA-256 padding.
152#[kernel]
153pub fn prf_block_kernel(input: PrfBlockInput) -> ShaBlock {
154    let mut bytes = [b8(0); SHA_BLOCK_BYTES];
155    bytes[0] = b8(0x03);
156    for index in 0..HASH_BYTES {
157        bytes[index + 1] = input.seed[index];
158    }
159    bytes[33] = (input.index >> 8).resize();
160    bytes[34] = input.index.resize();
161    bytes[35] = b8(0x80);
162    bytes[62] = b8(0x01);
163    bytes[63] = b8(0x18);
164    bytes_to_words_be_kernel(bytes)
165}
166
167/// Build the first, unpadded data block of `function_key || secret_prf`.
168#[kernel]
169pub fn secret_data_block_kernel(input: SecretDataInput) -> ShaBlock {
170    let mut bytes = [b8(0); SHA_BLOCK_BYTES];
171    for index in 0..HASH_BYTES {
172        bytes[index] = input.function_key[index];
173        bytes[index + HASH_BYTES] = input.secret_prf[index];
174    }
175    bytes_to_words_be_kernel(bytes)
176}
177
178/// Build the fixed second padding block for every 64-byte secret preimage.
179///
180/// The dummy bit is intentional: a zero-width RHDL kernel input is not
181/// representable, while the output is independent of its value.
182#[kernel]
183#[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
184pub fn secret_padding_block_kernel(_dummy: b1) -> ShaBlock {
185    let mut words = [b32(0); 16];
186    words[0] = b32(0x8000_0000);
187    words[15] = b32(512);
188    words
189}
190
191/// Build the padded 32-byte chain preimage `state XOR mask`.
192#[allow(clippy::needless_range_loop)] // RHDL kernels require indexable fixed arrays.
193#[kernel]
194pub fn chain_block_kernel(input: ChainBlockInput) -> ShaBlock {
195    let mut preimage = [b8(0); HASH_BYTES];
196    for index in 0..HASH_BYTES {
197        preimage[index] = input.state[index] ^ input.mask[index];
198    }
199    sha256_32_byte_block_kernel(preimage)
200}
201
202/// Build one ordinary, full data block from two adjacent endpoints.
203///
204/// This kernel serves public-key hash block indices `0..=32`.
205#[kernel]
206pub fn endpoint_pair_block_kernel(input: EndpointPairInput) -> ShaBlock {
207    let mut bytes = [b8(0); SHA_BLOCK_BYTES];
208    for index in 0..HASH_BYTES {
209        bytes[index] = input.first[index];
210        bytes[index + HASH_BYTES] = input.second[index];
211    }
212    bytes_to_words_be_kernel(bytes)
213}
214
215/// Build final public-key hash block 33 from endpoint 66 and fixed padding.
216#[kernel]
217pub fn endpoint_final_block_kernel(endpoint_66: HashBytes) -> ShaBlock {
218    let mut bytes = [b8(0); SHA_BLOCK_BYTES];
219    for index in 0..HASH_BYTES {
220        bytes[index] = endpoint_66[index];
221    }
222    bytes[HASH_BYTES] = b8(0x80);
223    bytes[62] = b8(0x43);
224    bytes[63] = b8(0x00);
225    bytes_to_words_be_kernel(bytes)
226}
227
228/// Independently construct one SHA-256 padded byte block in software.
229///
230/// This is an oracle for tests and integration models, not a hardware path.
231/// It returns `None` for an invalid block index or an unrepresentable bit
232/// length.
233#[must_use]
234pub fn software_padded_block(message: &[u8], block_index: usize) -> Option<[u32; 16]> {
235    let bit_length = u64::try_from(message.len()).ok()?.checked_mul(8)?;
236    let complete_blocks = message.len() / SHA_BLOCK_BYTES;
237    let remainder = message.len() % SHA_BLOCK_BYTES;
238    let block_count = complete_blocks + if remainder <= 55 { 1 } else { 2 };
239    if block_index >= block_count {
240        return None;
241    }
242
243    let mut bytes = [0_u8; SHA_BLOCK_BYTES];
244    let stream_offset = block_index.checked_mul(SHA_BLOCK_BYTES)?;
245    let length_offset = block_count.checked_mul(SHA_BLOCK_BYTES)?.checked_sub(8)?;
246    for (offset, output) in bytes.iter_mut().enumerate() {
247        let absolute = stream_offset + offset;
248        *output = if absolute < message.len() {
249            message[absolute]
250        } else if absolute == message.len() {
251            0x80
252        } else if absolute >= length_offset {
253            bit_length.to_be_bytes()[absolute - length_offset]
254        } else {
255            0
256        };
257    }
258    Some(software_words_from_bytes(bytes))
259}
260
261/// Convert a software byte block into SHA-256 big-endian words.
262#[must_use]
263pub fn software_words_from_bytes(bytes: [u8; SHA_BLOCK_BYTES]) -> [u32; 16] {
264    core::array::from_fn(|word| {
265        let offset = word * 4;
266        u32::from_be_bytes([
267            bytes[offset],
268            bytes[offset + 1],
269            bytes[offset + 2],
270            bytes[offset + 3],
271        ])
272    })
273}
274
275const _: () = {
276    assert!(HASH_INPUT_BITS == 256);
277    assert!(PRF_INPUT_BITS == 35 * 8);
278    assert!(SECRET_INPUT_BITS == 512);
279    assert!(ENDPOINT_STREAM_BYTES == 67 * HASH_BYTES);
280    assert!(ENDPOINT_STREAM_BITS == 17_152);
281};