Generalize KeyGen from K=2-hardcoded to compile-time parameter K:
- eta1 derived (3 for K=2, else 2); slot layout SLOT_S/E/T = K*K+{0,K,2K},
NUM_SLOTS = K*K+3K; SAW=5 slot-addr width.
- A-stage: explicit a_i/a_j row-major counters (slot = i*K+j) instead of
K=2 bit-tricks. C/N stages: parametric slot bases, 2K polys.
- M-stage: m_i/m_j widened to 3-bit (must reach K=4); slots i*K+j etc.
- E-stage: 2K polys, e_is_dk split, rho offset 384*K.
- H(ek): H_NBLK=ceil((EK_BYTES+1)/136), H_LAST padding generalized;
h_blk 4-bit. Byte mems sized EK_BYTES/DK_BYTES.
- Widen dbg_byte_idx_i to [10:0] (ek up to 1568B for K=4).
Parametric TB (tb_mlkem_kg_katK, KP generic + CASE plusarg). Verified
byte-exact vs NIST KAT:
K=2 (512): cases 0..4 ek 800B / dk 1632B
K=3 (768): cases 0..2 ek 1184B / dk 2400B (~36k cyc)
K=4 (1024): cases 0..2 ek 1568B / dk 3168B (~54k cyc)
run_tb.sh top runs all three parameter sets.
ML-KEM Hardware Implementation (FIPS 203)
A synchronous, pipelined hardware implementation of ML-KEM (Module-Lattice-based Key Encapsulation Mechanism), the NIST PQC standard based on Kyber. Written in SystemVerilog, targeting FPGA simulation with Vivado XSIM and verified with Verilator.
Overview
ML-KEM is a post-quantum key encapsulation mechanism (KEM) standardized by NIST in FIPS 203. It provides IND-CCA2 security based on the hardness of the Module Learning With Errors (MLWE) problem over the polynomial ring Z_q[x]/(x^256 + 1).
This implementation decomposes ML-KEM's core operations into independent, synchronous hardware modules with standardized valid/ready streaming interfaces. All modules operate at 100 MHz (10ns period) and use active-low reset.
Parameters
| Parameter | Value | Description |
|---|---|---|
| q | 3329 | Prime modulus |
| n | 256 | Polynomial degree |
| k | 2 | Module rank (ML-KEM-512) |
| η₁ | 3 | CBD parameter (secret key) |
| η₂ | 2 | CBD parameter (ciphertext) |
| d_u | 10 | Compress bits |
| d_v | 4 | Compress bits |
Repository Structure
mlkem/
├── sync_rtl/ # RTL source (SystemVerilog)
│ ├── common/ # Shared infrastructure
│ │ ├── pipeline_reg.v # Single-stage valid/ready pipeline register
│ │ ├── skid_buffer.v # 2-entry skid buffer for backpressure
│ │ └── defines.vh # Global parameters (Q, N, CLK_PERIOD)
│ ├── sha3/ # Keccak-f[1600] and SHA-3/SHAKE modes
│ │ ├── keccak_round.v # Single Keccak-f round (θ,ρ,π,χ,ι)
│ │ ├── keccak_core.v # 24-round sequential Keccak-f[1600] core
│ │ └── sha3_top.v # SHA3-512(G)/SHA3-256(H)/SHAKE-256(J) wrapper
│ ├── rng/ # Pseudorandom number generator
│ │ └── rng_sync.v # 256-bit Galois LFSR (taps: 255,253,252,247,0)
│ ├── ntt/ # Number Theoretic Transform
│ │ ├── zeta_rom.v # Twiddle factor ROM (128 × 12-bit, ζ^br(i))
│ │ ├── barrett_mul.v # Barrett modular multiplier (a·b mod q)
│ │ ├── butterfly_unit.v # CT/GS butterfly (NTT/INTT)
│ │ └── ntt_core.v # NTT core: LOAD→COMPUTE→OUTPUT FSM
│ ├── poly_arith/ # Polynomial arithmetic
│ │ └── poly_arith_sync.v # Element-wise poly add/sub (PolyAdd/PolySub)
│ ├── poly_mul/ # Polynomial multiplication
│ │ ├── poly_mul_zeta_rom.v # Zeta ROM for degree-1 basecase multiply
│ │ ├── basecase_mul.v # Degree-1 Karatsuba basecase multiplier
│ │ └── poly_mul_sync.v # Full NTT-domain polynomial multiplier
│ ├── sample_cbd/ # Centered Binomial Distribution sampling
│ │ └── sample_cbd_sync.v # CBDη via SHAKE-256 PRF(seed, nonce)
│ ├── sample_ntt/ # NTT-domain sampling (A matrix)
│ │ └── sample_ntt_sync.v # SampleNTT via SHAKE-128 rejection sampling
│ ├── comp_decomp/ # Coefficient compression
│ │ └── comp_decomp_sync.v # Compress_q / Decompress_q
│ ├── mod_add/ # Modular arithmetic
│ │ └── mod_add_sync.v # (a + b) mod q, streaming
│ └── storage/ # On-chip storage
│ ├── s_bram.v # Single-port behavioral BRAM
│ └── sd_bram.v # Simple dual-port behavioral BRAM
│
├── test_framework/ # Verilator C++ test framework
│ ├── run_all.py # CLI entry point
│ ├── config.json # Verilator path, clock period, timeouts
│ ├── lib/ # Core framework libraries
│ │ ├── test_runner.py # Discovery, compile, run, compare pipeline
│ │ ├── sim_controller.py # Verilator compile/run wrapper
│ │ ├── vector_gen.py # Base class for vector generators
│ │ ├── result_checker.py # Hex-file comparison
│ │ └── reporter.py # Terminal + HTML output
│ └── modules/ # Per-module test definitions
│ ├── <module>/test_plan.json # Test configuration
│ └── <module>/gen_vectors.py # Python reference + vector generator
│
├── run_tb.sh # Vivado XSIM testbench runner
├── .trellis/ # Trellis workflow system
│ ├── workflow.md # Development phases
│ ├── spec/ # Coding specs (RTL, testbench conventions)
│ └── tasks/ # Active and archived tasks
└── .opencode/ # OpenCode agent configuration
Module Architecture
Core Operations
┌──────────┐
seed │ sample_ │ coeffs (256 × 12-bit)
nonce ─┤ cbd_sync ├─────────────────────┐
└──────────┘ │
▼
┌──────────┐ ┌─────────────┐
rho │ sample_ │ coeffs │ poly_arith │
k,i,j─┤ ntt_sync ├─────────────┤ poly_mul │──► result
└──────────┘ │ comp_decomp │
└─────────────┘
┌──────────┐ ▲
d_in │ sha3_ │ rho, sigma │
│ chain_top├────────────────────┘
└──────────┘
Interface Protocol
All modules use a uniform valid/ready streaming interface:
clk ──╮ ╰──╮ ╰──╮ ╰──╮ ╰──
valid_i ──╯ ╰─────╯ ╰─────
ready_o ──────╮ ╰─────────
data_i ──[A]─────[B]─────────[C]──
valid_o ─────────╮ ╰───────
ready_i ─────────────╮ ╰─────────
data_o ─────────[A']───────[B']──
- Input: Assert
valid_iwhenready_ois high; data transferred on posedge when both are high. - Output: Module asserts
valid_owhen result is ready; downstream assertsready_ito consume. - Pipeline: Modules use
pipeline_reginternally for 1-cycle latency.
Modules with multi-cycle operations (NTT, sampling) additionally use a done_o signal or last_o flag.
Getting Started
Prerequisites
- Vivado 2019.2+ (for XSIM simulation):
/opt/Xilinx/Vivado/2019.2/ - Verilator 5.046 (for C++ testbench): available via
dnfon Fedora - Python 3.10+ (for vector generation): stdlib only
Setup
# Clone repository
git clone <repo-url> mlkem
cd mlkem
# Source Vivado (for XSIM)
source /opt/Xilinx/Vivado/2019.2/settings64.sh
export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # ncurses fix for 2019.2 on modern Linux
Running Tests
Vivado XSIM (Verilog Testbench)
# List available modules
./run_tb.sh --list
# Run a specific module
./run_tb.sh mod_add
./run_tb.sh ntt
./run_tb.sh sample_cbd
# Run all modules
for m in mod_add rng poly_arith comp_decomp storage \
ntt poly_mul sample_cbd sample_ntt; do
./run_tb.sh "$m"
done
Each module's testbench is in sync_rtl/<module>/TB/:
tb_<module>_xsim.v— Verilog testbench (file-based vectors via$readmemh)gen_vectors.py— Python vector generatorvectors/<module>_input.hex— Test input vectorsxsim_run.tcl— Vivado compile/elaborate/simulate script
Verilator (C++ Testbench)
cd test_framework
python3 run_all.py --list # List modules
python3 run_all.py --module ntt # Test a single module
python3 run_all.py --quick # Smoke test all modules
Manual XSIM Commands
# Compile
xvlog -sv -i . sync_rtl/common/pipeline_reg.v sync_rtl/mod_add/mod_add_sync.v
xvlog -sv sync_rtl/mod_add/TB/tb_mod_add_xsim.v
# Elaborate
xelab tb_mod_add_xsim -s sim --timescale 1ns/1ps
# Simulate
xsim sim -R
Design Decisions
Synchronous Valid/Ready Streaming
All modules use a synchronous valid/ready handshake rather than fixed-latency interfaces. This allows:
- Natural backpressure propagation
- Easy composition of modules in pipelines
- Deterministic timing closure at 100MHz
Barrett Modular Reduction
All modular multiplications use Barrett reduction (no DSP blocks, no division units):
- Precompute μ = ⌊2^k / q⌋ (k = 24 for q=3329)
- Compute a·b ≈ (a·b·μ) >> k, then correct with conditional subtraction
- Fully combinational, no pipeline stalls
Cooley-Tukey / Gentleman-Sande NTT
The NTT core implements both forward (Cooley-Tukey) and inverse (Gentleman-Sande) transforms using a radix-2 decimation-in-time architecture:
- 7 butterfly stages (256 = 2^7 coefficients)
- Bit-reversed input/output ordering
- On-chip coefficient register file (256 × 12-bit)
- 24-cycle pipeline for Keccak permutations (shared with SHA-3 modules)
Keccak-f[1600] Core
A single Keccak-f[1600] permutation engine is shared across all SHA-3/SHAKE modules (sha3_top, sample_cbd_sync, sample_ntt_sync). The core implements:
- 24 rounds with round constants (ι step)
- Full 1600-bit state (5×5×64 lanes)
- 24-cycle latency per permutation
- Input/output via valid/ready streaming interface
Module Reference
| Module | Ports | Latency | Description |
|---|---|---|---|
pipeline_reg |
data_i/o, valid_i/o, ready_i/o | 1 cycle | Generic pipeline stage |
skid_buffer |
data_i/o, valid_i/o, ready_i/o | 0-1 cycles | Backpressure buffer |
rng_sync |
valid_i → data_o[255:0] | 1 cycle | Galois LFSR PRNG |
mod_add_sync |
a[11:0], b[11:0] → sum[11:0] | 1 cycle | Modular addition |
ntt_core |
256×coeff_in → 256×coeff_out | ~200 cycles | NTT/INTT transform |
poly_arith_sync |
coeff_a/b[11:0] → coeff_out[11:0] | 1 cycle | Poly add/sub |
poly_mul_sync |
512×coeff → 256×coeff | ~300 cycles | NTT-domain poly multiply |
comp_decomp_sync |
coeff_in[11:0], d[4:0] → coeff_out | 1 cycle | Compress/Decompress |
sha3_top |
data_i[511:0], mode → hash_o[511:0] | ~24 cycles | SHA3/SHAKE |
sample_cbd_sync |
seed[255:0], nonce, eta → 256×coeff | ~300 cycles | CBD sampling |
sample_ntt_sync |
rho[255:0], k,i,j → 256×coeff | ~4000 cycles | SampleNTT |
s_bram |
rd/wr addr, data | 1 cycle | Single-port BRAM |
sd_bram |
rd addr, wr addr, data | 1 cycle | Dual-port BRAM |
Test Coverage
| Module | Verilator (C++) | XSIM (Verilog) | Status |
|---|---|---|---|
| sha3_top | ✅ | ✅ | PASS |
| keccak_core | — | ✅ | PASS |
| rng_sync | ✅ | ✅ | PASS |
| mod_add_sync | ✅ | ✅ | PASS |
| ntt_core | ✅ | ✅ | PASS |
| poly_arith_sync | ✅ | ✅ | PASS |
| poly_mul_sync | ✅ | ✅ | PASS |
| comp_decomp_sync | ✅ | ✅ | PASS |
| sample_cbd_sync | ✅ | ✅ | PASS |
| sample_ntt_sync | ✅ | ✅ | PASS |
| s_bram / sd_bram | ✅ | ✅ | PASS |
| pipeline_reg | Through parent | — | OK |
| skid_buffer | Through parent | — | OK |
Vivado 2019.2 Compatibility Notes
This project was tested with Vivado 2019.2 on Fedora 44. Known workarounds:
# Required: ncurses compatibility library
export LD_PRELOAD=/usr/lib64/libtinfo.so.5
# Use -i flag (not -include_dirs) for include paths
xvlog -sv -i . <file>.v
# Add --timescale to xelab
xelab <top> -s <snap> --timescale 1ns/1ps
# Add --relax for strict SystemVerilog mode
xvlog -sv --relax <file>.v
TODO / Roadmap
- Top-level integration module (full KeyGen / Encaps / Decaps FSM)
- AXI-Stream bridge for FPGA integration
- Resource optimization (share Keccak instances, pipeline balancing)
- Formal verification of Barrett multiplier
- Power analysis and side-channel hardening
- XDC constraints for FPGA synthesis (timing, I/O)
License
[Specify license]
References
- FIPS 203: ML-KEM — NIST standard
- FIPS 202: SHA-3 / SHAKE — Keccak-based hash
- CRYSTALS-Kyber — Original submission