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}