Skip to main content

rhdl_dsig_scalable_core/
haraka.rs

1//! Haraka-512 v2 and the 144-bit DSig chain-compression primitive.
2//!
3//! # Why this module exists
4//!
5//! DSig signs with W-OTS+ (a Winternitz one-time signature, here with `w = 4`).
6//! Its secret key is 68 short *chains*. Each chain starts at an 18-byte
7//! (144-bit) secret node, and every step along the chain hashes the current
8//! node once. A signature needs 204 of these chain steps, so the chain hash is
9//! the single most-repeated operation in the whole signer.
10//!
11//! The `DSIG_FPGA_W4_HARAKA_BLAKE3_V1` profile uses **Haraka-512 v2** for that
12//! hash. Haraka is built entirely from AES rounds, which suits hardware: an AES
13//! round is a few table lookups and XORs, and it pipelines well. This module is
14//! the *meaning* of Haraka in this crate. It holds:
15//!
16//! - a plain-Rust reference ([`haraka512`], [`dsig_chain`]) that the tests and
17//!   the host use as an oracle;
18//! - the constants (round keys, the word mix, the truncation) shared by every
19//!   hardware variant;
20//! - small RHDL kernels for each piece of an AES round;
21//! - a general 20-stage pipeline ([`Haraka512Pipeline`]) that hashes any block.
22//!
23//! The signer itself uses a specialised copy in [`crate::chain_step`], which
24//! reuses the kernels and constants here but folds away the constant bytes of
25//! a chain block. The S-box memories live in [`crate::aes_table`].
26//! [`crate::chain_lane`] picks which chain hash is compiled in.
27//!
28//! # Haraka-512 in one picture
29//!
30//! The 64-byte state is four 16-byte *AES lanes*. (Here "lane" means a 16-byte
31//! quarter of the state. Elsewhere in the crate a "lane" is a whole hardware
32//! chain-step pipeline; the context makes clear which.) One Haraka round is two
33//! AES rounds on every AES lane, then a fixed shuffle of 32-bit words across
34//! lanes ([`MIX_WORDS`]). There are five Haraka rounds:
35//!
36//! ```text
37//!   input (64 B) ─┬───────────────────────────────────────────────────────┐
38//!                 ▼                                                       │
39//!   AES lane 0  [AES][AES]┐                                               │
40//!   AES lane 1  [AES][AES]├─ MIX ─▶ ... repeat: 5 Haraka rounds in all     │
41//!   AES lane 2  [AES][AES]│        = 5 × 2 AES rounds × 4 lanes            │
42//!   AES lane 3  [AES][AES]┘        = 40 AES rounds                         │
43//!                 │                                                       │
44//!                 ▼                                                       ▼
45//!   final state ── pick 32 bytes (DIGEST_BYTES) ──▶ XOR ◀── same 32 input bytes
46//!                                                    │       ("feed-forward")
47//!                                                    ▼
48//!                                          32-byte Haraka-512 digest
49//!                                                    │
50//!                                          first 18 bytes = next DSig chain node
51//! ```
52//!
53//! Each `[AES]` box is one AES round (`AESENC`), drawn in detail in
54//! [`crate::aes_table`]: SubBytes → ShiftRows → MixColumns → AddRoundKey.
55//! The feed-forward (XOR the input back in) is what turns the reversible AES
56//! permutation into a one-way compression function.
57//!
58//! # From the picture to a pipeline
59//!
60//! Each AES round is cut into two registered halves: the S-box lookups on one
61//! side, the XOR network of MixColumns and the key on the other. That way an
62//! S-box can be a synchronous ROM whose own output register is the stage
63//! register (see [`crate::aes_table`]). Ten AES rounds give 20 stages
64//! ([`HARAKA_PIPELINE_LATENCY`]):
65//!
66//! ```text
67//!   stage:   0          1                2          3                     ...  19
68//!          SubBytes   MixColumns       SubBytes   MixColumns
69//!          +ShiftRows +AddRoundKey     +ShiftRows +AddRoundKey + MIX
70//!          (round 0)  (round 0)        (round 1)  (round 1)
71//!            reg        reg              reg        reg                        reg
72//! ```
73//!
74//! Even stages are [`sub_shift512_kernel`]; odd stages are
75//! [`mix_key512_kernel`], and every fourth stage (3, 7, 11, 15, 19) also
76//! applies the word mix. A new block can enter every clock (initiation
77//! interval, "II", of 1), and its digest leaves 20 clocks later.
78//!
79//! # Reference, pipeline and provenance
80//!
81//! [`haraka512`](crate::haraka::haraka512) is a byte-oriented, allocation-free reference. The reference
82//! uses field arithmetic independently of the hardware S-box. It is an oracle,
83//! not a side-channel-hardened software signing implementation. The RHDL
84//! [`Haraka512Pipeline`](crate::haraka::Haraka512Pipeline) has twenty registered stages: each AES round is split
85//! between SubBytes/ShiftRows and MixColumns/AddRoundKey. Haraka's inter-lane
86//! permutation is wiring following every second AES round. A separate valid
87//! pipeline resets ownership; resetless state and tags are never meaningful
88//! while `valid` is false. No throughput, clock, or resource figure here is a
89//! synthesis or hardware measurement.
90//!
91//! Constants, lane permutations, and the official known-answer vector follow
92//! [kste/haraka](https://github.com/kste/haraka), Haraka v2. DSig's
93//! `mid_haraka512` selects exactly the first eighteen bytes of the standard
94//! 32-byte output, **not** the first eighteen bytes of the full 64-byte state.
95//! The salt layout follows DSig commit
96//! `1647bfbb12c42a249d68ce4909f81c74baab69bc`
97//! (`dsig/src/util.hpp`, `third-party/src/haraka/haraka.h`).
98//!
99//! Derived Haraka constants/permutation attribution: Copyright (c) 2016 kste.
100//! The MIT License (MIT): Permission is hereby granted, free of charge, to any
101//! person obtaining a copy of this software and associated documentation files
102//! (the "Software"), to deal in the Software without restriction, including
103//! without limitation the rights to use, copy, modify, merge, publish,
104//! distribute, sublicense, and/or sell copies of the Software, and to permit
105//! persons to whom the Software is furnished to do so, subject to the following
106//! conditions: The above copyright notice and this permission notice shall be
107//! included in all copies or substantial portions of the Software. THE SOFTWARE
108//! IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
109//! INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
110//! A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
111//! COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
112//! WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
113//! IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
114
115use rhdl::prelude::*;
116use rhdl_fpga::core::dff::DFF;
117
118/// AES rounds per Haraka-512 invocation (two rounds per mixing layer).
119///
120/// Five Haraka rounds × two AES rounds = 10. Each of these rounds runs on all
121/// four 16-byte AES lanes at once, so one hash is 40 AES-round evaluations.
122pub const AES_ROUNDS: usize = 10;
123/// Registered transfers per complete hash; there is no input backpressure.
124///
125/// Two stages per AES round (SubBytes/ShiftRows, then MixColumns/AddRoundKey),
126/// so 20 clock cycles from input to digest.
127pub const HARAKA_PIPELINE_LATENCY: usize = 2 * AES_ROUNDS;
128
129/// Haraka v2 constants as integers whose little-endian bytes match AES-NI.
130///
131/// Upstream `_mm_set_epi32(a, b, c, d)` is represented as the integer
132/// `0xaaaabbbbccccdddd`: the first byte in memory is the low byte of `d`.
133///
134/// 40 entries of 128 bits: entry `4·round + lane` is the AddRoundKey value for
135/// AES lane `lane` in AES round `round`. Haraka uses fixed public constants
136/// instead of a secret key; they only break the symmetry between rounds and
137/// lanes.
138pub const ROUND_CONSTANTS: [u128; 40] = [
139    0x0684704c_e620c00a_b2c5fef0_75817b9d,
140    0x8b66b4e1_88f3a06b_640f6ba4_2f08f717,
141    0x3402de2d_53f28498_cf029d60_9f029114,
142    0x0ed6eae6_2e7b4f08_bbf3bcaf_fd5b4f79,
143    0xcbcfb0cb_4872448b_79eecd1c_be397044,
144    0x7eeacdee_6e9032b7_8d5335ed_2b8a057b,
145    0x67c28f43_5e2e7cd0_e2412761_da4fef1b,
146    0x2924d9b0_afcacc07_675ffde2_1fc70b3b,
147    0xab4d63f1_e6867fe9_ecdb8fca_b9d465ee,
148    0x1c30bf84_d4b7cd64_5b2a404f_ad037e33,
149    0xb2cc0bb9_941723bf_69028b2e_8df69800,
150    0xfa0478a6_de6f5572_4aaa9ec8_5c9d2d8a,
151    0xdfb49f2b_6b772a12_0efa4f2e_29129fd4,
152    0x1ea10344_f449a236_32d611ae_bb6a12ee,
153    0xaf044988_4b050084_5f9600c9_9ca8eca6,
154    0x21025ed8_9d199c4f_78a2c7e3_27e593ec,
155    0xbf3aaaf8_a759c9b7_b9282ecd_82d40173,
156    0x6260700d_6186b017_37f2efd9_10307d6b,
157    0x5aca45c2_21300443_81c29153_f6fc9ac6,
158    0x9223973c_226b68bb_2caf92e8_36d1943a,
159    0xd3bf9238_225886eb_6cbab958_e51071b4,
160    0xdb863ce5_aef0c677_933dfddd_24e1128d,
161    0xbb606268_ffeba09c_83e48de3_cb2212b1,
162    0x734bd3dc_e2e4d19c_2db91a4e_c72bf77d,
163    0x43bb47c3_61301b43_4b1415c4_2cb3924e,
164    0xdba775a8_e707eff6_03b231dd_16eb6899,
165    0x6df3614b_3c755977_8e5e2302_7eca472c,
166    0xcda75a17_d6de7d77_6d1be5b9_b88617f9,
167    0xec6b43f0_6ba8e9aa_9d6c069d_a946ee5d,
168    0xcb1e6950_f957332b_a2531159_3bf327c1,
169    0x2cee0c75_00da619c_e4ed0353_600ed0d9,
170    0xf0b1a5a1_96e90cab_80bbbabc_63a4a350,
171    0xae3db102_5e962988_ab0dde30_938dca39,
172    0x17bb8f38_d554a40b_8814f3a8_2e75b442,
173    0x34bb8a5b_5f427fd7_aeb6b779_360a16f6,
174    0x26f65241_cbe55438_43ce5918_ffbaafde,
175    0x4ce99a54_b9f3026a_a2ca9cf7_839ec978,
176    0xae51a51a_1bdff7be_40c06e28_22901235,
177    0xa0c1613c_ba7ed22b_c173bc0f_48a659cf,
178    0x756acc03_02288288_4ad6bdfd_e9c59da1,
179];
180
181/// Source-word positions for the Haraka-512 32-bit inter-lane permutation.
182///
183/// The state is 16 words of 4 bytes. After the mix, word `w` holds what was
184/// word `MIX_WORDS[w]`. This is the only thing that moves data between AES
185/// lanes, and it is pure wiring in hardware (no logic at all).
186pub const MIX_WORDS: [usize; 16] = [3, 11, 7, 15, 8, 0, 12, 4, 9, 1, 13, 5, 2, 10, 6, 14];
187/// Source-byte positions of the standard 256-bit feed-forward/truncation.
188///
189/// Digest byte `i` is `final[DIGEST_BYTES[i]] ⊕ input[DIGEST_BYTES[i]]`. The
190/// picked bytes are the upper half of AES lanes 0 and 1 (bytes 8..16 and
191/// 24..32) and the lower half of lanes 2 and 3 (bytes 32..40 and 48..56).
192/// DSig keeps the first 18 of them, which come from only five of the sixteen
193/// 32-bit words; [`crate::chain_step`] uses that to skip unobserved S-boxes.
194pub const DIGEST_BYTES: [usize; 32] = [
195    8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
196    48, 49, 50, 51, 52, 53, 54, 55,
197];
198
199/// Re-pack [`ROUND_CONSTANTS`] as RHDL bytes, one 64-byte row per AES round.
200///
201/// Row `round` holds the four lanes' 16-byte keys side by side, matching the
202/// 64-byte state layout, so a stage can XOR it in byte by byte.
203#[allow(clippy::cast_lossless)] // From<u8> is not available in a const context.
204const fn rhdl_round_keys() -> [[b8; 64]; AES_ROUNDS] {
205    // result[round][16·lane + byte]: the finished table.
206    let mut result = [[b8(0); 64]; AES_ROUNDS];
207    let mut round = 0;
208    while round < AES_ROUNDS {
209        let mut lane = 0;
210        while lane < 4 {
211            // This lane's 128-bit key as 16 bytes in AES-NI memory order.
212            let bytes = ROUND_CONSTANTS[4 * round + lane].to_le_bytes();
213            let mut byte = 0;
214            while byte < 16 {
215                result[round][16 * lane + byte] = b8(bytes[byte] as u128);
216                byte += 1;
217            }
218            lane += 1;
219        }
220        round += 1;
221    }
222    result
223}
224
225/// Four compile-time AES round keys per registered linear stage.
226///
227/// `ROUND_KEYS_RHDL[r]` is the 64-byte AddRoundKey value of AES round `r`.
228/// Being constants, the key XORs become inverters or nothing at all in
229/// synthesis.
230pub const ROUND_KEYS_RHDL: [[b8; 64]; AES_ROUNDS] = rhdl_round_keys();
231
232/// Multiply two bytes in the AES field GF(2^8) (compile-time reference).
233///
234/// GF(2^8) is AES's byte arithmetic: add is XOR, and multiply is
235/// shift-and-add reduced by `x^8 + x^4 + x^3 + x + 1` (`0x1b`). This is the
236/// slow, obviously-correct form, used only to build reference tables.
237const fn gf_multiply(mut left: u8, mut right: u8) -> u8 {
238    // Running product (XOR is addition in GF(2^8)).
239    let mut product = 0;
240    // Which bit of `right` is being processed (8 in all).
241    let mut bit = 0;
242    while bit < 8 {
243        // If this bit of `right` is set, add the current multiple of `left`.
244        if right & 1 != 0 {
245            product ^= left;
246        }
247        // Multiply `left` by x (i.e. 2), reducing when the top bit falls off.
248        left = (left << 1) ^ if left & 0x80 == 0 { 0 } else { 0x1b };
249        right >>= 1;
250        bit += 1;
251    }
252    product
253}
254
255/// Independent reference AES S-box via inversion in GF(2^8) and affine map.
256///
257/// The AES S-box is `S(x) = A(x^254) ⊕ 0x63`, where `x^254` is the field
258/// inverse of `x` (and 0 maps to 0) and `A` is a fixed bit-rotation mix.
259/// Computing it from the definition, rather than copying a table, gives the
260/// tests an oracle that does not share a source with [`aes_sbox_kernel`].
261#[must_use]
262pub const fn aes_sbox(value: u8) -> u8 {
263    // Square-and-multiply for value^254: `inverse` accumulates the result,
264    // `power` walks value^1, value^2, value^4, ..., `exponent` is what's left.
265    let mut inverse = 1;
266    let mut power = value;
267    let mut exponent = 254;
268    while exponent != 0 {
269        if exponent & 1 != 0 {
270            inverse = gf_multiply(inverse, power);
271        }
272        power = gf_multiply(power, power);
273        exponent >>= 1;
274    }
275    // The AES affine map: XOR of the byte with four left rotations, plus 0x63.
276    inverse
277        ^ inverse.rotate_left(1)
278        ^ inverse.rotate_left(2)
279        ^ inverse.rotate_left(3)
280        ^ inverse.rotate_left(4)
281        ^ 0x63
282}
283
284/// Evaluate [`aes_sbox`] for all 256 inputs, at compile time.
285const fn sbox_table() -> [u8; 256] {
286    // table[x] = S(x).
287    let mut table = [0; 256];
288    let mut x = 0;
289    while x < 256 {
290        table[x] = aes_sbox(x as u8);
291        x += 1;
292    }
293    table
294}
295
296/// [`aes_sbox`] evaluated once for every input at compile time.
297///
298/// The field-arithmetic definition stays the independent oracle; this table
299/// only makes host-side verification of millions of signatures fast.
300pub const SBOX: [u8; 256] = sbox_table();
301
302/// One reference AESENC round, including MixColumns and the supplied key.
303///
304/// `state` and `key` are 16 bytes in AES column-major order: byte
305/// `4·column + row`. This is the software meaning of one `[AES]` box in the
306/// module picture; the hardware splits it into [`aes_sub_shift_kernel`] and
307/// [`aes_mix_key_kernel`].
308#[must_use]
309pub fn aes_round(state: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
310    // SubBytes and ShiftRows together: output (column, row) is the S-box of
311    // input (column + row mod 4, row).
312    let mut shifted = [0; 16];
313    for column in 0..4 {
314        for row in 0..4 {
315            shifted[4 * column + row] = SBOX[usize::from(state[4 * ((column + row) % 4) + row])];
316        }
317    }
318    // MixColumns and AddRoundKey: each output byte is
319    // 2·s[row] ⊕ 3·s[row+1] ⊕ s[row+2] ⊕ s[row+3] ⊕ key, within its column.
320    let mut result = [0; 16];
321    for column in 0..4 {
322        for row in 0..4 {
323            // Index of the column's first byte.
324            let base = 4 * column;
325            result[base + row] = gf_multiply(shifted[base + row], 2)
326                ^ gf_multiply(shifted[base + (row + 1) % 4], 3)
327                ^ shifted[base + (row + 2) % 4]
328                ^ shifted[base + (row + 3) % 4]
329                ^ key[base + row];
330        }
331    }
332    result
333}
334
335/// Pure Rust Haraka-512 v2 with fixed standard constants and 32-byte output.
336///
337/// This fixed-input-length compression function is not a general-purpose
338/// variable-length hash and must not be extended by ad-hoc concatenation.
339///
340/// Input: exactly 64 bytes. Output: the standard 32-byte digest. This is the
341/// oracle that every hardware pipeline is compared against.
342#[must_use]
343pub fn haraka512(input: &[u8; 64]) -> [u8; 32] {
344    // The 64-byte working state, four AES lanes of 16 bytes.
345    let mut state = *input;
346    for round in 0..AES_ROUNDS {
347        // One AES round on each of the four lanes, with that lane's key.
348        for lane in 0..4 {
349            // A copy of this lane's 16 bytes.
350            let mut block = [0; 16];
351            block.copy_from_slice(&state[16 * lane..16 * (lane + 1)]);
352            // The lane after one AES round.
353            let next = aes_round(&block, &ROUND_CONSTANTS[4 * round + lane].to_le_bytes());
354            state[16 * lane..16 * (lane + 1)].copy_from_slice(&next);
355        }
356        // After every second AES round (rounds 1, 3, 5, 7, 9): the word mix.
357        if round % 2 == 1 {
358            // Snapshot, because the permutation reads and writes the state.
359            let before = state;
360            for word in 0..16 {
361                state[4 * word..4 * word + 4]
362                    .copy_from_slice(&before[4 * MIX_WORDS[word]..4 * MIX_WORDS[word] + 4]);
363            }
364        }
365    }
366    // Truncate to 32 bytes and XOR the same input bytes back in (feed-forward).
367    core::array::from_fn(|byte| state[DIGEST_BYTES[byte]] ^ input[DIGEST_BYTES[byte]])
368}
369
370/// DSig's 144-bit Haraka output, exactly the prefix of [`haraka512`].
371///
372/// 18 bytes, the size of one DSig chain node (`ELEMENT_BYTES` in the crate
373/// root).
374#[must_use]
375pub fn mid_haraka512(input: &[u8; 64]) -> [u8; 18] {
376    // The full 32-byte digest; keep bytes 0..18.
377    let digest = haraka512(input);
378    core::array::from_fn(|byte| digest[byte])
379}
380
381/// Builds DSig's 64-byte chain block with wrapping low-32-bit nonce salt.
382///
383/// Layout: nonce (16 bytes), secret (18 bytes), zero padding (30 bytes).
384/// Only nonce bytes 0..4 change: their little-endian value adds
385/// `index + 68 * depth` modulo 2^32. There is no carry into nonce byte four.
386/// This low-level oracle deliberately accepts all indices/depths; the WOTS
387/// protocol must separately enforce `index < 68` and its legal chain depth.
388///
389/// ```text
390///  byte  0    4          16              34                          64
391///        ├────┼──────────┼───────────────┼───────────────────────────┤
392///        │salt│ nonce    │ secret (node) │  30 zero bytes            │
393///        │ed  │ 4..16    │ 18 bytes      │                           │
394///        └────┴──────────┴───────────────┴───────────────────────────┘
395///   bytes 0..4 = (nonce[0..4] as u32 LE) + index + 68·depth   (mod 2^32)
396/// ```
397///
398/// Why salt at all: adding `index + 68·depth` makes every (chain, step) pair
399/// hash a block with a different prefix, so no two hash calls in one key
400/// share an input shape. `index` is the chain number (0..68) and `depth` is
401/// how far along the chain the step is.
402#[must_use]
403#[allow(clippy::cast_possible_truncation)] // Protocol explicitly reduces modulo 2^32.
404pub fn dsig_chain_block(
405    secret: &[u8; 18],
406    nonce: &[u8; 16],
407    index: usize,
408    depth: usize,
409) -> [u8; 64] {
410    // Start from all zeros; bytes 34..64 stay zero.
411    let mut block = [0; 64];
412    block[..16].copy_from_slice(nonce);
413    // The nonce's low 32 bits, little-endian.
414    let low = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
415    // The salt for this chain and depth, wrapping at 2^32.
416    let offset = (index as u32).wrapping_add(68_u32.wrapping_mul(depth as u32));
417    // Overwrite bytes 0..4 with the salted value (no carry into byte 4).
418    block[..4].copy_from_slice(&low.wrapping_add(offset).to_le_bytes());
419    block[16..34].copy_from_slice(secret);
420    block
421}
422
423/// Applies one DSig chain step at the given index and depth (Haraka-512 in
424/// the V1 profile; see [`chain_lane`](crate::chain_lane)).
425///
426/// Input: the current 18-byte node, the 16-byte key nonce, the chain index
427/// and the depth. Output: the next 18-byte node. This is the oracle that
428/// `tests/chain_step.rs` compares the hardware lane against.
429#[must_use]
430pub fn dsig_chain(secret: &[u8; 18], nonce: &[u8; 16], index: usize, depth: usize) -> [u8; 18] {
431    // The compiled-in chain hash (Haraka-512 unless a `chain-*` feature says otherwise).
432    crate::chain_lane::software_chain(&dsig_chain_block(secret, nonce, index, depth))
433}
434
435/// RHDL byte packing for the exact [`dsig_chain_block`] salt domain.
436///
437/// The index and depth are reduced modulo 2^32 by their input types; the
438/// caller must enforce WOTS protocol bounds independently. This function only
439/// packs the block and does not perform a hash or allocate a one-time key.
440///
441/// Inputs: the 18-byte node, the 16-byte nonce, and 32-bit `index` and
442/// `depth`. Output: the 64-byte chain block. Combinational: one 32-bit
443/// add-and-multiply-by-constant, the rest is wiring.
444#[allow(clippy::needless_range_loop)] // Static byte wiring.
445#[kernel]
446pub fn dsig_chain_block_kernel(
447    secret: [b8; 18],
448    nonce: [b8; 16],
449    index: b32,
450    depth: b32,
451) -> [b8; 64] {
452    // Zero block; bytes 34..64 are never written and stay zero.
453    let mut block = [b8(0); 64];
454    for byte in 0..16 {
455        block[byte] = nonce[byte];
456    }
457    // Assemble the nonce's low 32 bits, little-endian (byte 0 is least
458    // significant). RHDL needs explicit widening before each shift.
459    let low: b32 = nonce[0].resize()
460        | (nonce[1].resize::<32>() << 8)
461        | (nonce[2].resize::<32>() << 16)
462        | (nonce[3].resize::<32>() << 24);
463    // Add the salt. `b32` arithmetic wraps at 2^32, matching the reference.
464    let salted = low + index + b32(68) * depth;
465    // Split the salted word back into bytes 0..4.
466    block[0] = salted.resize();
467    block[1] = (salted >> 8).resize();
468    block[2] = (salted >> 16).resize();
469    block[3] = (salted >> 24).resize();
470    // The 18-byte node goes into bytes 16..34.
471    for byte in 0..18 {
472        block[16 + byte] = secret[byte];
473    }
474    block
475}
476
477/// Exhaustive combinational AES S-box for synthesis as fixed Boolean logic.
478///
479/// Input and output are one byte. A 256-way `match` is the most direct way
480/// to tell RHDL "this is a fixed table"; synthesis turns it into LUT logic.
481/// The chain-step lane uses ROMs from [`crate::aes_table`] instead, so the
482/// memory type can be chosen per S-box; this kernel serves the general
483/// [`Haraka512Pipeline`] and the small equivalence kernels.
484#[allow(clippy::too_many_lines)] // The complete permutation has 256 cases.
485#[kernel]
486pub fn aes_sbox_kernel(value: b8) -> b8 {
487    // One arm per input byte, values from the AES standard (FIPS-197).
488    match value {
489        Bits::<8>(0x00) => b8(0x63),
490        Bits::<8>(0x01) => b8(0x7c),
491        Bits::<8>(0x02) => b8(0x77),
492        Bits::<8>(0x03) => b8(0x7b),
493        Bits::<8>(0x04) => b8(0xf2),
494        Bits::<8>(0x05) => b8(0x6b),
495        Bits::<8>(0x06) => b8(0x6f),
496        Bits::<8>(0x07) => b8(0xc5),
497        Bits::<8>(0x08) => b8(0x30),
498        Bits::<8>(0x09) => b8(0x01),
499        Bits::<8>(0x0a) => b8(0x67),
500        Bits::<8>(0x0b) => b8(0x2b),
501        Bits::<8>(0x0c) => b8(0xfe),
502        Bits::<8>(0x0d) => b8(0xd7),
503        Bits::<8>(0x0e) => b8(0xab),
504        Bits::<8>(0x0f) => b8(0x76),
505        Bits::<8>(0x10) => b8(0xca),
506        Bits::<8>(0x11) => b8(0x82),
507        Bits::<8>(0x12) => b8(0xc9),
508        Bits::<8>(0x13) => b8(0x7d),
509        Bits::<8>(0x14) => b8(0xfa),
510        Bits::<8>(0x15) => b8(0x59),
511        Bits::<8>(0x16) => b8(0x47),
512        Bits::<8>(0x17) => b8(0xf0),
513        Bits::<8>(0x18) => b8(0xad),
514        Bits::<8>(0x19) => b8(0xd4),
515        Bits::<8>(0x1a) => b8(0xa2),
516        Bits::<8>(0x1b) => b8(0xaf),
517        Bits::<8>(0x1c) => b8(0x9c),
518        Bits::<8>(0x1d) => b8(0xa4),
519        Bits::<8>(0x1e) => b8(0x72),
520        Bits::<8>(0x1f) => b8(0xc0),
521        Bits::<8>(0x20) => b8(0xb7),
522        Bits::<8>(0x21) => b8(0xfd),
523        Bits::<8>(0x22) => b8(0x93),
524        Bits::<8>(0x23) => b8(0x26),
525        Bits::<8>(0x24) => b8(0x36),
526        Bits::<8>(0x25) => b8(0x3f),
527        Bits::<8>(0x26) => b8(0xf7),
528        Bits::<8>(0x27) => b8(0xcc),
529        Bits::<8>(0x28) => b8(0x34),
530        Bits::<8>(0x29) => b8(0xa5),
531        Bits::<8>(0x2a) => b8(0xe5),
532        Bits::<8>(0x2b) => b8(0xf1),
533        Bits::<8>(0x2c) => b8(0x71),
534        Bits::<8>(0x2d) => b8(0xd8),
535        Bits::<8>(0x2e) => b8(0x31),
536        Bits::<8>(0x2f) => b8(0x15),
537        Bits::<8>(0x30) => b8(0x04),
538        Bits::<8>(0x31) => b8(0xc7),
539        Bits::<8>(0x32) => b8(0x23),
540        Bits::<8>(0x33) => b8(0xc3),
541        Bits::<8>(0x34) => b8(0x18),
542        Bits::<8>(0x35) => b8(0x96),
543        Bits::<8>(0x36) => b8(0x05),
544        Bits::<8>(0x37) => b8(0x9a),
545        Bits::<8>(0x38) => b8(0x07),
546        Bits::<8>(0x39) => b8(0x12),
547        Bits::<8>(0x3a) => b8(0x80),
548        Bits::<8>(0x3b) => b8(0xe2),
549        Bits::<8>(0x3c) => b8(0xeb),
550        Bits::<8>(0x3d) => b8(0x27),
551        Bits::<8>(0x3e) => b8(0xb2),
552        Bits::<8>(0x3f) => b8(0x75),
553        Bits::<8>(0x40) => b8(0x09),
554        Bits::<8>(0x41) => b8(0x83),
555        Bits::<8>(0x42) => b8(0x2c),
556        Bits::<8>(0x43) => b8(0x1a),
557        Bits::<8>(0x44) => b8(0x1b),
558        Bits::<8>(0x45) => b8(0x6e),
559        Bits::<8>(0x46) => b8(0x5a),
560        Bits::<8>(0x47) => b8(0xa0),
561        Bits::<8>(0x48) => b8(0x52),
562        Bits::<8>(0x49) => b8(0x3b),
563        Bits::<8>(0x4a) => b8(0xd6),
564        Bits::<8>(0x4b) => b8(0xb3),
565        Bits::<8>(0x4c) => b8(0x29),
566        Bits::<8>(0x4d) => b8(0xe3),
567        Bits::<8>(0x4e) => b8(0x2f),
568        Bits::<8>(0x4f) => b8(0x84),
569        Bits::<8>(0x50) => b8(0x53),
570        Bits::<8>(0x51) => b8(0xd1),
571        Bits::<8>(0x52) => b8(0x00),
572        Bits::<8>(0x53) => b8(0xed),
573        Bits::<8>(0x54) => b8(0x20),
574        Bits::<8>(0x55) => b8(0xfc),
575        Bits::<8>(0x56) => b8(0xb1),
576        Bits::<8>(0x57) => b8(0x5b),
577        Bits::<8>(0x58) => b8(0x6a),
578        Bits::<8>(0x59) => b8(0xcb),
579        Bits::<8>(0x5a) => b8(0xbe),
580        Bits::<8>(0x5b) => b8(0x39),
581        Bits::<8>(0x5c) => b8(0x4a),
582        Bits::<8>(0x5d) => b8(0x4c),
583        Bits::<8>(0x5e) => b8(0x58),
584        Bits::<8>(0x5f) => b8(0xcf),
585        Bits::<8>(0x60) => b8(0xd0),
586        Bits::<8>(0x61) => b8(0xef),
587        Bits::<8>(0x62) => b8(0xaa),
588        Bits::<8>(0x63) => b8(0xfb),
589        Bits::<8>(0x64) => b8(0x43),
590        Bits::<8>(0x65) => b8(0x4d),
591        Bits::<8>(0x66) => b8(0x33),
592        Bits::<8>(0x67) => b8(0x85),
593        Bits::<8>(0x68) => b8(0x45),
594        Bits::<8>(0x69) => b8(0xf9),
595        Bits::<8>(0x6a) => b8(0x02),
596        Bits::<8>(0x6b) => b8(0x7f),
597        Bits::<8>(0x6c) => b8(0x50),
598        Bits::<8>(0x6d) => b8(0x3c),
599        Bits::<8>(0x6e) => b8(0x9f),
600        Bits::<8>(0x6f) => b8(0xa8),
601        Bits::<8>(0x70) => b8(0x51),
602        Bits::<8>(0x71) => b8(0xa3),
603        Bits::<8>(0x72) => b8(0x40),
604        Bits::<8>(0x73) => b8(0x8f),
605        Bits::<8>(0x74) => b8(0x92),
606        Bits::<8>(0x75) => b8(0x9d),
607        Bits::<8>(0x76) => b8(0x38),
608        Bits::<8>(0x77) => b8(0xf5),
609        Bits::<8>(0x78) => b8(0xbc),
610        Bits::<8>(0x79) => b8(0xb6),
611        Bits::<8>(0x7a) => b8(0xda),
612        Bits::<8>(0x7b) => b8(0x21),
613        Bits::<8>(0x7c) => b8(0x10),
614        Bits::<8>(0x7d) => b8(0xff),
615        Bits::<8>(0x7e) => b8(0xf3),
616        Bits::<8>(0x7f) => b8(0xd2),
617        Bits::<8>(0x80) => b8(0xcd),
618        Bits::<8>(0x81) => b8(0x0c),
619        Bits::<8>(0x82) => b8(0x13),
620        Bits::<8>(0x83) => b8(0xec),
621        Bits::<8>(0x84) => b8(0x5f),
622        Bits::<8>(0x85) => b8(0x97),
623        Bits::<8>(0x86) => b8(0x44),
624        Bits::<8>(0x87) => b8(0x17),
625        Bits::<8>(0x88) => b8(0xc4),
626        Bits::<8>(0x89) => b8(0xa7),
627        Bits::<8>(0x8a) => b8(0x7e),
628        Bits::<8>(0x8b) => b8(0x3d),
629        Bits::<8>(0x8c) => b8(0x64),
630        Bits::<8>(0x8d) => b8(0x5d),
631        Bits::<8>(0x8e) => b8(0x19),
632        Bits::<8>(0x8f) => b8(0x73),
633        Bits::<8>(0x90) => b8(0x60),
634        Bits::<8>(0x91) => b8(0x81),
635        Bits::<8>(0x92) => b8(0x4f),
636        Bits::<8>(0x93) => b8(0xdc),
637        Bits::<8>(0x94) => b8(0x22),
638        Bits::<8>(0x95) => b8(0x2a),
639        Bits::<8>(0x96) => b8(0x90),
640        Bits::<8>(0x97) => b8(0x88),
641        Bits::<8>(0x98) => b8(0x46),
642        Bits::<8>(0x99) => b8(0xee),
643        Bits::<8>(0x9a) => b8(0xb8),
644        Bits::<8>(0x9b) => b8(0x14),
645        Bits::<8>(0x9c) => b8(0xde),
646        Bits::<8>(0x9d) => b8(0x5e),
647        Bits::<8>(0x9e) => b8(0x0b),
648        Bits::<8>(0x9f) => b8(0xdb),
649        Bits::<8>(0xa0) => b8(0xe0),
650        Bits::<8>(0xa1) => b8(0x32),
651        Bits::<8>(0xa2) => b8(0x3a),
652        Bits::<8>(0xa3) => b8(0x0a),
653        Bits::<8>(0xa4) => b8(0x49),
654        Bits::<8>(0xa5) => b8(0x06),
655        Bits::<8>(0xa6) => b8(0x24),
656        Bits::<8>(0xa7) => b8(0x5c),
657        Bits::<8>(0xa8) => b8(0xc2),
658        Bits::<8>(0xa9) => b8(0xd3),
659        Bits::<8>(0xaa) => b8(0xac),
660        Bits::<8>(0xab) => b8(0x62),
661        Bits::<8>(0xac) => b8(0x91),
662        Bits::<8>(0xad) => b8(0x95),
663        Bits::<8>(0xae) => b8(0xe4),
664        Bits::<8>(0xaf) => b8(0x79),
665        Bits::<8>(0xb0) => b8(0xe7),
666        Bits::<8>(0xb1) => b8(0xc8),
667        Bits::<8>(0xb2) => b8(0x37),
668        Bits::<8>(0xb3) => b8(0x6d),
669        Bits::<8>(0xb4) => b8(0x8d),
670        Bits::<8>(0xb5) => b8(0xd5),
671        Bits::<8>(0xb6) => b8(0x4e),
672        Bits::<8>(0xb7) => b8(0xa9),
673        Bits::<8>(0xb8) => b8(0x6c),
674        Bits::<8>(0xb9) => b8(0x56),
675        Bits::<8>(0xba) => b8(0xf4),
676        Bits::<8>(0xbb) => b8(0xea),
677        Bits::<8>(0xbc) => b8(0x65),
678        Bits::<8>(0xbd) => b8(0x7a),
679        Bits::<8>(0xbe) => b8(0xae),
680        Bits::<8>(0xbf) => b8(0x08),
681        Bits::<8>(0xc0) => b8(0xba),
682        Bits::<8>(0xc1) => b8(0x78),
683        Bits::<8>(0xc2) => b8(0x25),
684        Bits::<8>(0xc3) => b8(0x2e),
685        Bits::<8>(0xc4) => b8(0x1c),
686        Bits::<8>(0xc5) => b8(0xa6),
687        Bits::<8>(0xc6) => b8(0xb4),
688        Bits::<8>(0xc7) => b8(0xc6),
689        Bits::<8>(0xc8) => b8(0xe8),
690        Bits::<8>(0xc9) => b8(0xdd),
691        Bits::<8>(0xca) => b8(0x74),
692        Bits::<8>(0xcb) => b8(0x1f),
693        Bits::<8>(0xcc) => b8(0x4b),
694        Bits::<8>(0xcd) => b8(0xbd),
695        Bits::<8>(0xce) => b8(0x8b),
696        Bits::<8>(0xcf) => b8(0x8a),
697        Bits::<8>(0xd0) => b8(0x70),
698        Bits::<8>(0xd1) => b8(0x3e),
699        Bits::<8>(0xd2) => b8(0xb5),
700        Bits::<8>(0xd3) => b8(0x66),
701        Bits::<8>(0xd4) => b8(0x48),
702        Bits::<8>(0xd5) => b8(0x03),
703        Bits::<8>(0xd6) => b8(0xf6),
704        Bits::<8>(0xd7) => b8(0x0e),
705        Bits::<8>(0xd8) => b8(0x61),
706        Bits::<8>(0xd9) => b8(0x35),
707        Bits::<8>(0xda) => b8(0x57),
708        Bits::<8>(0xdb) => b8(0xb9),
709        Bits::<8>(0xdc) => b8(0x86),
710        Bits::<8>(0xdd) => b8(0xc1),
711        Bits::<8>(0xde) => b8(0x1d),
712        Bits::<8>(0xdf) => b8(0x9e),
713        Bits::<8>(0xe0) => b8(0xe1),
714        Bits::<8>(0xe1) => b8(0xf8),
715        Bits::<8>(0xe2) => b8(0x98),
716        Bits::<8>(0xe3) => b8(0x11),
717        Bits::<8>(0xe4) => b8(0x69),
718        Bits::<8>(0xe5) => b8(0xd9),
719        Bits::<8>(0xe6) => b8(0x8e),
720        Bits::<8>(0xe7) => b8(0x94),
721        Bits::<8>(0xe8) => b8(0x9b),
722        Bits::<8>(0xe9) => b8(0x1e),
723        Bits::<8>(0xea) => b8(0x87),
724        Bits::<8>(0xeb) => b8(0xe9),
725        Bits::<8>(0xec) => b8(0xce),
726        Bits::<8>(0xed) => b8(0x55),
727        Bits::<8>(0xee) => b8(0x28),
728        Bits::<8>(0xef) => b8(0xdf),
729        Bits::<8>(0xf0) => b8(0x8c),
730        Bits::<8>(0xf1) => b8(0xa1),
731        Bits::<8>(0xf2) => b8(0x89),
732        Bits::<8>(0xf3) => b8(0x0d),
733        Bits::<8>(0xf4) => b8(0xbf),
734        Bits::<8>(0xf5) => b8(0xe6),
735        Bits::<8>(0xf6) => b8(0x42),
736        Bits::<8>(0xf7) => b8(0x68),
737        Bits::<8>(0xf8) => b8(0x41),
738        Bits::<8>(0xf9) => b8(0x99),
739        Bits::<8>(0xfa) => b8(0x2d),
740        Bits::<8>(0xfb) => b8(0x0f),
741        Bits::<8>(0xfc) => b8(0xb0),
742        Bits::<8>(0xfd) => b8(0x54),
743        Bits::<8>(0xfe) => b8(0xbb),
744        // The only remaining input is 0xff, whose S-box value is 0x16.
745        _ => b8(0x16),
746    }
747}
748
749/// AES field multiplication by x, reduced modulo x^8 + x^4 + x^3 + x + 1.
750///
751/// "Multiply by x" is "multiply by 2" in GF(2^8): shift left one bit, and if
752/// bit 7 was set, XOR in `0x1b`. In hardware that is wiring plus three XOR
753/// gates, which is why MixColumns is cheap compared with an S-box.
754#[kernel]
755pub fn aes_xtime_kernel(value: b8) -> b8 {
756    // Shift left, then conditionally reduce by the AES polynomial.
757    (value << 1)
758        ^ if (value & b8(0x80)) != b8(0) {
759            b8(0x1b)
760        } else {
761            b8(0)
762        }
763}
764
765/// First half of an AESENC round, with conventional AES column-major bytes.
766///
767/// SubBytes then ShiftRows on one 16-byte AES lane: 16 S-boxes, and wiring.
768/// Combinational; the pipeline puts a register after it.
769#[allow(clippy::needless_range_loop)] // Static loops describe replicated hardware.
770#[kernel]
771pub fn aes_sub_shift_kernel(state: [b8; 16]) -> [b8; 16] {
772    // Every byte is overwritten below; starting from `state` just gives the
773    // array a value.
774    let mut result = state;
775    for column in 0..4 {
776        for row in 0..4 {
777            // ShiftRows source: same row, column (column + row) mod 4.
778            result[4 * column + row] = aes_sbox_kernel(state[4 * ((column + row) & 3) + row]);
779        }
780    }
781    result
782}
783
784/// Second half of AESENC: MixColumns followed by AddRoundKey.
785///
786/// Uses the XOR-saving form of MixColumns:
787/// `out[r] = s[r] ⊕ t ⊕ 2·(s[r] ⊕ s[r+1]) ⊕ key[r]`, with `t` the XOR of
788/// the whole column. It equals the textbook `2·s[r] ⊕ 3·s[r+1] ⊕ s[r+2] ⊕
789/// s[r+3]` but shares `t` across the four rows. Combinational, XOR only.
790#[allow(clippy::needless_range_loop)] // Static loops describe replicated hardware.
791#[kernel]
792pub fn aes_mix_key_kernel(state: [b8; 16], key: [b8; 16]) -> [b8; 16] {
793    // Placeholder value; every byte is overwritten.
794    let mut result = state;
795    for column in 0..4 {
796        // Index of the column's first byte.
797        let base = 4 * column;
798        // XOR of all four bytes of the column, shared by its four outputs.
799        let total = state[base] ^ state[base + 1] ^ state[base + 2] ^ state[base + 3];
800        for row in 0..4 {
801            result[base + row] = state[base + row]
802                ^ total
803                ^ aes_xtime_kernel(state[base + row] ^ state[base + ((row + 1) & 3)])
804                ^ key[base + row];
805        }
806    }
807    result
808}
809
810/// One complete AESENC round for small-stage lowering/equivalence gates.
811///
812/// Both halves back to back, with no register between them. Not used in the
813/// pipelines; it exists so a single AES round can be lowered and checked
814/// against [`aes_round`] cheaply.
815#[kernel]
816pub fn aes_round_kernel(state: [b8; 16], key: [b8; 16]) -> [b8; 16] {
817    aes_mix_key_kernel(aes_sub_shift_kernel(state), key)
818}
819
820/// Pure wiring of the sixteen 32-bit Haraka lane words.
821///
822/// Applies [`MIX_WORDS`] to a 64-byte state. No gates: synthesis just
823/// reconnects wires.
824#[allow(clippy::needless_range_loop)] // Compile-time permutation.
825#[kernel]
826pub fn mix512_kernel(state: [b8; 64]) -> [b8; 64] {
827    // Placeholder value; every byte is overwritten.
828    let mut result = state;
829    for word in 0..16 {
830        for byte in 0..4 {
831            // Output word `word` takes the bytes of input word MIX_WORDS[word].
832            result[4 * word + byte] = state[4 * MIX_WORDS[word] + byte];
833        }
834    }
835    result
836}
837
838/// Parallel nonlinear half-round for all four AES lanes.
839///
840/// `SubBytes ∘ ShiftRows` on the full 64-byte state: 64 S-boxes. This is
841/// the function every [`SboxStage`](crate::aes_table::SboxStage) computes,
842/// and the fast simulation model of that stage calls it directly.
843#[allow(clippy::needless_range_loop)] // Static four-lane replication.
844#[kernel]
845pub fn sub_shift512_kernel(state: [b8; 64]) -> [b8; 64] {
846    // Placeholder value; every byte is overwritten.
847    let mut result = state;
848    for lane in 0..4 {
849        // Cut out AES lane `lane` (16 bytes).
850        let mut block = [b8(0); 16];
851        for byte in 0..16 {
852            block[byte] = state[16 * lane + byte];
853        }
854        // Run the half-round on it and paste it back.
855        let next = aes_sub_shift_kernel(block);
856        for byte in 0..16 {
857            result[16 * lane + byte] = next[byte];
858        }
859    }
860    result
861}
862
863/// Parallel linear half-round with optional post-round inter-lane wiring.
864///
865/// `MixColumns ∘ AddRoundKey` on all four AES lanes, using `keys` (a row of
866/// [`ROUND_KEYS_RHDL`]). When `mix` is true, the [`MIX_WORDS`] shuffle
867/// follows. In the pipelines `mix` is always a compile-time constant, so the
868/// final `if` costs nothing.
869#[allow(clippy::needless_range_loop)] // Static four-lane replication.
870#[kernel]
871pub fn mix_key512_kernel(state: [b8; 64], keys: [b8; 64], mix: bool) -> [b8; 64] {
872    // Placeholder value; every byte is overwritten.
873    let mut result = state;
874    for lane in 0..4 {
875        // This AES lane's 16 state bytes and its 16 key bytes.
876        let mut block = [b8(0); 16];
877        let mut key = [b8(0); 16];
878        for byte in 0..16 {
879            block[byte] = state[16 * lane + byte];
880            key[byte] = keys[16 * lane + byte];
881        }
882        let next = aes_mix_key_kernel(block, key);
883        for byte in 0..16 {
884            result[16 * lane + byte] = next[byte];
885        }
886    }
887    // After every second AES round, mix words across lanes.
888    if mix { mix512_kernel(result) } else { result }
889}
890
891/// One input token; every asserted valid bit is accepted on the next edge.
892///
893/// A *token* is one unit of work moving down the pipeline: here, one block
894/// to hash plus its bookkeeping. The pipeline has no "ready" signal (no
895/// backpressure), so the sender must never offer more than one per clock.
896#[derive(Clone, Copy, Debug, PartialEq, Digital)]
897pub struct HarakaInput {
898    /// Exactly 64 input bytes in conventional memory order.
899    pub block: [b8; 64],
900    /// Opaque caller context identifier, preserved without modification.
901    pub tag: b32,
902    /// This token owns a result; false inserts a bubble.
903    pub valid: bool,
904}
905
906/// The idle input: an all-zero block with `valid` false (a bubble).
907impl Default for HarakaInput {
908    /// A bubble: nothing is launched.
909    fn default() -> Self {
910        Self {
911            block: [b8(0); 64],
912            tag: b32(0),
913            valid: false,
914        }
915    }
916}
917
918/// Final digest token; all fields except `valid` are unspecified when invalid.
919///
920/// Appears exactly [`HARAKA_PIPELINE_LATENCY`] clocks after its input.
921#[derive(Clone, Copy, Debug, PartialEq, Digital)]
922pub struct HarakaOutput {
923    /// Standard 32-byte Haraka v2 result; take bytes 0..18 for DSig chains.
924    pub digest: [b8; 32],
925    /// Caller tag aligned with the digest.
926    pub tag: b32,
927    /// A result is available and has not been invalidated by reset.
928    pub valid: bool,
929}
930
931/// Prefix of the twenty-stage Haraka pipeline, for bounded validation.
932///
933/// `STAGES` must be between one and twenty inclusive. Only the full twenty
934/// stages implement Haraka-512. Shorter instances expose the truncated,
935/// feed-forwarded intermediate state for focused equivalence tests; they must
936/// never be used as a cryptographic hash. Every specialization accepts one
937/// token per cycle without backpressure and delays validity by `STAGES`
938/// registered transfers. Reset flushes every in-flight token.
939///
940/// Four parallel shift registers, all `STAGES` deep, move in lock step:
941///
942/// ```text
943///   input ─▶ [stage 0] ─▶ [stage 1] ─▶ ... ─▶ [stage STAGES-1] ─▶ digest
944///   state:    sub/shift    mix/key             (computed)
945///   feed_forward, tag, valid: copied unchanged from stage to stage
946/// ```
947///
948/// Only `valid` has a reset value. The data registers do not, which keeps
949/// them cheap (no reset fan-out) and is safe because nothing reads data
950/// whose `valid` is false.
951#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
952pub struct HarakaPipeline<const STAGES: usize> {
953    /// The 64-byte working state of each stage, after that stage's half-round.
954    state: [DFF<[b8; 64]>; STAGES],
955    /// The 32 input bytes the final XOR needs, carried alongside the state so
956    /// they arrive at the end at the same time as their token.
957    feed_forward: [DFF<[b8; 32]>; STAGES],
958    /// The caller's 32-bit tag, delayed to match.
959    tag: [DFF<b32>; STAGES],
960    /// "This stage holds a real token." The only registers that reset.
961    valid: [DFF<bool>; STAGES],
962}
963
964/// Build a pipeline of `STAGES` stages, checking that `STAGES` is legal.
965impl<const STAGES: usize> Default for HarakaPipeline<STAGES> {
966    /// Data registers start as "don't care"; every valid bit starts false.
967    ///
968    /// # Panics
969    /// Panics if `STAGES` is outside `1..=20`.
970    fn default() -> Self {
971        assert!((1..=HARAKA_PIPELINE_LATENCY).contains(&STAGES));
972        Self {
973            state: core::array::from_fn(|_| DFF::resetless()),
974            feed_forward: core::array::from_fn(|_| DFF::resetless()),
975            tag: core::array::from_fn(|_| DFF::resetless()),
976            valid: core::array::from_fn(|_| DFF::new(false)),
977        }
978    }
979}
980
981/// Ports of [`HarakaPipeline`]: one block in, one digest out per clock.
982impl<const STAGES: usize> SynchronousIO for HarakaPipeline<STAGES> {
983    /// A block, a tag and a valid bit.
984    type I = HarakaInput;
985    /// A digest, the same tag and a valid bit, `STAGES` clocks later.
986    type O = HarakaOutput;
987    /// The stage wiring below.
988    type Kernel = haraka_pipeline_kernel<STAGES>;
989}
990
991/// Complete Haraka-512 v2 pipeline with twenty-cycle registered latency.
992pub type Haraka512Pipeline = HarakaPipeline<HARAKA_PIPELINE_LATENCY>;
993
994/// Connectivity of the resetless datapath and resettable ownership pipeline.
995///
996/// Inputs: `clock_reset`, the new token `input`, and `q` (every register's
997/// current value). Outputs: the token leaving the last stage, and `d` (every
998/// register's next value). Timing: a token entering at clock `t` leaves at
999/// clock `t + STAGES`. Stage 0 computes on `input` directly; stage `s > 0`
1000/// computes on the register of stage `s - 1`.
1001#[allow(clippy::needless_range_loop)] // Compile-time pipeline elaboration.
1002#[kernel]
1003pub fn haraka_pipeline_kernel<const STAGES: usize>(
1004    clock_reset: ClockReset,
1005    input: HarakaInput,
1006    q: HarakaPipelineQ<STAGES>,
1007) -> (HarakaOutput, HarakaPipelineD<STAGES>) {
1008    // True while reset is asserted; used to hide the output during reset.
1009    let resetting = clock_reset.reset.any();
1010    // Start with "every register keeps its value"; each stage is set below.
1011    let mut d = HarakaPipelineD::<STAGES> {
1012        state: q.state,
1013        feed_forward: q.feed_forward,
1014        tag: q.tag,
1015        valid: q.valid,
1016    };
1017    // Stage 0: round 0's SubBytes/ShiftRows on the incoming block.
1018    d.state[0] = sub_shift512_kernel(input.block);
1019    // Capture the 32 input bytes the final feed-forward XOR will need.
1020    for byte in 0..32 {
1021        d.feed_forward[0][byte] = input.block[DIGEST_BYTES[byte]];
1022    }
1023    d.tag[0] = input.tag;
1024    d.valid[0] = input.valid;
1025    // Stages 1..STAGES: alternate the two half-rounds.
1026    for stage in 1..STAGES {
1027        if stage & 1 == 0 {
1028            // Even stage: SubBytes/ShiftRows of AES round stage/2.
1029            d.state[stage] = sub_shift512_kernel(q.state[stage - 1]);
1030        } else {
1031            // Odd stage: MixColumns + key of AES round stage/2 (`>> 1`),
1032            // plus the word mix after every second AES round (stage 3, 7, ...).
1033            d.state[stage] = mix_key512_kernel(
1034                q.state[stage - 1],
1035                ROUND_KEYS_RHDL[stage >> 1],
1036                stage & 3 == 3,
1037            );
1038        }
1039        // Side-band data just moves one stage along.
1040        d.feed_forward[stage] = q.feed_forward[stage - 1];
1041        d.tag[stage] = q.tag[stage - 1];
1042        d.valid[stage] = q.valid[stage - 1];
1043    }
1044    // Output: truncate the last stage's state and XOR in the saved input bytes.
1045    let mut digest = [b8(0); 32];
1046    for byte in 0..32 {
1047        digest[byte] = q.state[STAGES - 1][DIGEST_BYTES[byte]] ^ q.feed_forward[STAGES - 1][byte];
1048    }
1049    // `valid` is forced low during reset so no stale token escapes.
1050    (
1051        HarakaOutput {
1052            digest,
1053            tag: q.tag[STAGES - 1],
1054            valid: q.valid[STAGES - 1] && !resetting,
1055        },
1056        d,
1057    )
1058}
1059
1060/// Generates the complete twenty-stage pipeline's RHDL-authored Verilog.
1061///
1062/// # Errors
1063/// Returns an RHDL lowering/rendering/syntax-check error. Successful rendering
1064/// alone is not synthesis, timing closure, equivalence, or hardware evidence.
1065///
1066/// This is how the RHDL source becomes Verilog for Vivado: RHDL "lowers" the
1067/// kernels to its intermediate form and prints Verilog modules.
1068pub fn haraka512_verilog() -> Result<String, RHDLError> {
1069    haraka_pipeline_verilog::<HARAKA_PIPELINE_LATENCY>()
1070}
1071
1072/// Generates a registered prefix for focused lowering and RTL tests.
1073///
1074/// # Panics
1075/// Panics if `STAGES` is outside 1..=20.
1076///
1077/// # Errors
1078/// Returns any RHDL lowering/rendering/syntax-check error.
1079pub fn haraka_pipeline_verilog<const STAGES: usize>() -> Result<String, RHDLError> {
1080    // Build the circuit, describe it under a fixed top-level module name,
1081    // lower it to HDL and print the modules.
1082    Ok(HarakaPipeline::<STAGES>::default()
1083        .descriptor("haraka512_pipeline".into())?
1084        .hdl()?
1085        .modules
1086        .pretty())
1087}