A small, synthesisable ChaCha20-Poly1305 AEAD IP core in SystemVerilog. Implements RFC 8439 / RFC 7539 end-to-end (ChaCha20 stream cipher, Poly1305 MAC, AEAD construction with AAD support, constant-time tag compare). Targets iCE40, ECP5, Xilinx 7-series, Cyclone V, and Tang Nano 9K.
The core is verified end-to-end against RFC 8439 published vectors and
100 random vectors cross-checked against Python's
cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305. No FPGA
hardware is required for the test suite.
ChaCha20-Poly1305 is the modern AEAD used by TLS 1.3, WireGuard, SSH, the Linux kernel, and most new protocols where AES-NI is not available or where constant-time AES is hard. Free FPGA cores for it are rare, incomplete, or shipped without testbenches. This core is small, dual-licensed cleanly (GPL for OSS, commercial for closed-source), shipped with a real handshake protocol, RFC 8439 vectors in the testbench, SystemVerilog assertions, functional coverage, and a synthesis-comparison report across multiple toolchains.
git clone https://github.com/ayoub-ac/chacha20poly1305-fpga-core.git
cd chacha20poly1305-fpga-core
make lint test # Verilator lint + run RFC 8439 + random vectors
make synth_report # SYNTH_REPORT.md across iCE40/ECP5/Xilinx/Vivado/QuartusRequires Verilator 5.0+ for simulation. Yosys 0.30+ for the open synth
flow. Python 3 with cryptography for make regen_vectors.
+-------------------------+
key_i ---->| |
nonce_i ---->| chacha20_poly1305_aead |---> result_o (ct/pt)
aad_i ---->| (top-level FSM) |---> result_byte_count_o
data_i ---->| |---> tag_o (128-bit)
init_i---->| |---> tag_match_o (CT compare)
finalize_i---->| ready_o / valid_o |
+------------+------------+
|
+----------------+-----------------+
| | |
chacha20_block chacha20_core poly1305_core
(poly key gen, (streaming (130-bit acc,
counter=0) counter=1) 128-bit r)
| |
+-> chacha20_qround (combinational)
Top-level FSM phases: IDLE -> DERIVE_KEY -> POLY_INIT -> AAD -> DATA -> DATA_FEED -> LEN -> FINALIZE -> DONE. The FSM handles
zero-padding of partial AAD / ciphertext chunks per RFC 8439 §2.8 and
emits the final (len_aad || len_ct) block to Poly1305 automatically.
| Signal | Dir | Width | Description | RFC ref |
|---|---|---|---|---|
clk_i |
in | 1 | System clock, all flops sample on rising edge. | - |
rst_ni |
in | 1 | Synchronous active-low reset; hold low ≥4 cycles before first command. | - |
init_i |
in | 1 | Pulse for one cycle to start a new session with key_i / nonce_i / mode_i. |
- |
key_i |
in | 256 | ChaCha20 256-bit key, byte 0 in key_i[7:0]. |
§2.4 |
nonce_i |
in | 96 | 96-bit nonce, byte 0 in nonce_i[7:0]. |
§2.3 |
mode_i |
in | 2 | 00=encrypt, 01=decrypt, 10=AAD-only. | - |
init_ready_o |
out | 1 | High when the core can accept a new init_i. |
- |
aad_valid_i |
in | 1 | Asserted with aad_chunk_i / aad_byte_count_i / aad_last_i to feed AAD. |
§2.8 |
aad_chunk_i |
in | 128 | 16-byte AAD chunk; bytes beyond aad_byte_count_i are masked internally. |
§2.8 |
aad_byte_count_i |
in | 5 | Valid bytes in aad_chunk_i (1..16). 16 means a full chunk. |
- |
aad_last_i |
in | 1 | Marks the final AAD chunk. | - |
aad_ready_o |
out | 1 | Core ready to accept an AAD beat (handshake: aad_valid_i && aad_ready_o). |
- |
data_valid_i |
in | 1 | Asserted with data_i / data_byte_count_i / data_last_i to feed plaintext / ciphertext. |
§2.4 |
data_i |
in | 512 | 64-byte data block; bytes beyond data_byte_count_i are don't-care. |
§2.4 |
data_byte_count_i |
in | 7 | Valid bytes in data_i (1..64). 64 means a full block. |
- |
data_last_i |
in | 1 | Marks the final data block. Triggers automatic finalisation. | - |
data_ready_o |
out | 1 | Core ready to accept a data beat. | - |
result_valid_o |
out | 1 | Result block valid; consumer must assert result_ready_i to consume. |
- |
result_o |
out | 512 | Output ciphertext (encrypt) or plaintext (decrypt) block. | - |
result_byte_count_o |
out | 7 | Valid bytes in result_o. |
- |
result_last_o |
out | 1 | High on the final result block. | - |
result_ready_i |
in | 1 | Master asserts to consume result_o. |
- |
finalize_i |
in | 1 | Pulse to finalise (alternative to data_last_i for streaming flows). |
- |
tag_o |
out | 128 | 128-bit Poly1305 tag, byte 0 in tag_o[7:0]. |
§2.5 |
tag_valid_o |
out | 1 | Tag is valid and stable. | - |
expected_tag_i |
in | 128 | Caller-provided expected tag for constant-time compare. | - |
tag_match_o |
out | 1 | Constant-time comparison: (tag_o == expected_tag_i) && tag_valid_o. |
- |
Real numbers from make synth_report (Yosys 0.x, AEAD top with all
sub-modules flattened):
| Target | LUT | FF | BRAM | Latency (114 B msg) | Notes |
|---|---|---|---|---|---|
| iCE40 UP5K | see SYNTH_REPORT.md |
~4500 | 0 | ~640 cycles | LUT4 generic, no DSP |
| ECP5 LFE5UM-25 | see SYNTH_REPORT.md |
~4500 | 0 | ~640 cycles | abc9, no DSP inference |
| Xilinx Artix-7 | see SYNTH_REPORT.md |
~4500 | 0 | ~640 cycles | Yosys generic mapping |
The 130x128 Poly1305 multiplier is the dominant area cost. Vendor flows
(Vivado / Diamond / Quartus) fold this into DSP slices and the gate
count drops sharply; synth_xilinx shown here uses the open generic
mapping. See RESOURCE_ESTIMATES.md for the
full breakdown and reduction options.
You need Verilator 5.0 or newer.
make lint # static check the RTL (qround + block + core + poly + aead)
make test # build and run AEAD test suite (RFC 8439 + 100 random)
make test-chacha # ChaCha20 streaming-core unit tests
make test-poly # Poly1305 unit tests
make test-all # all three suitesA passing run ends with +PASS all tests passed and exits 0.
Three orthogonal techniques are wired into the testbench.
| # | Test | Coverage |
|---|---|---|
| 1 | RFC 8439 §2.8.2 AEAD vector | Full AEAD encrypt + tag + tag-match |
| 2 | Tampered expected_tag rejected | tag_match_o constant-time compare |
| 3 | 100 cross-validated random vectors | cryptography.hazmat.primitives.ciphers.aead |
| 4 | Mode coverage (encrypt / decrypt / AAD-only) | All mode_i encodings |
| 5 | 1024-byte multi-block message | Streaming & length tracking |
Plus dedicated unit suites:
make test-chacha: RFC 8439 §2.4.2 sunscreen vector, §2.3.2 block-function vector, 50 random round-trips cross-checked against a software ChaCha20.make test-poly: RFC 8439 §2.5.2 vector, empty-message edge case, 100 random MACs cross-checked against a software Poly1305 reference.
tb/aead_assertions.sv enforces protocol invariants on every cycle of
every test:
data_ready_oonly rises afterinit_ihas been seen.result_ois stable while back-pressured (result_valid_o && !result_ready_i).tag_ois stable whiletag_valid_o.tag_valid_oonly rises after an effective finalisation (finalize_ipulse OR adata_last_ibeat).- Tag latency bound after finalisation (
< LAT_MAXcycles).
The assertions compile with both Verilator 5.x (--assert) and Vivado
xsim. They are stripped on synthesis flows via `ifndef SYNTHESIS.
tb/aead_cov.sv collects 10 bins. The simulator prints a coverage
summary at end-of-test:
---- Functional coverage ----
[HIT ] mode_encrypt / mode_decrypt / mode_aad_only
[HIT ] aad_seen / data_short / data_long
[HIT ] partial_last / full_last
[HIT ] finalize_seen / tag_emitted
Coverage: 10/10 bins (100.0%)
A regression that drops below 100% fails the gate.
make synth_report runs every available toolchain on the same RTL and
emits SYNTH_REPORT.md with a side-by-side LUT/FF/BRAM
table. Yosys (synth_ice40 / synth_ecp5 / synth_xilinx) is mandatory;
Vivado and Quartus are detected automatically and skipped with a notice
when not on $PATH.
| Variant | Use case | Tier |
|---|---|---|
rtl/chacha20_poly1305_aead.sv |
Default AEAD top: encrypt / decrypt / AAD-only, single clock domain | GPL |
rtl/chacha20_core.sv |
ChaCha20 stream cipher only (no MAC) — for protocols using Poly1305 separately | GPL |
rtl/poly1305_core.sv |
Poly1305 MAC only (no cipher) — for raw MAC use cases | GPL |
vhdl_wrapper/chacha20_poly1305_vhdl.vhd |
VHDL-2008 entity wrapping the SV core for VHDL-only designs | All |
Premium tier (on roadmap, not yet shipped):
- DSP-aware Poly1305 multiplier (Xilinx DSP48 / Lattice 18x18 instantiation).
- Side-channel hardening (masked ChaCha20 + masked Poly1305).
- XChaCha20-Poly1305 (192-bit nonce variant).
Dual-licensed:
- GPL-3.0-or-later for open-source projects. If your product links this RTL or its compiled bitstream, your project must also be GPL-3.0+.
- Commercial license for closed-source products. See
LICENSE.mdfor the legal text and an FAQ.
If unsure which applies, read LICENSE.md or open an issue.
rtl/ RTL sources
chacha20_qround.sv combinational quarter round
chacha20_block.sv single-block engine
chacha20_core.sv streaming cipher
poly1305_core.sv MAC engine
chacha20_poly1305_aead.sv AEAD top-level
tb/ testbench
sim_main.cpp C++ harness, RFC vectors, 5 test groups
sim_main_chacha.cpp ChaCha20 unit harness
sim_main_poly.cpp Poly1305 unit harness
aead_tb.sv DUT wrapper + assertion + coverage bind
aead_assertions.sv SVA properties
aead_cov.sv functional coverage collector
rfc8439_vectors.sv RFC 8439 vectors (for non-Verilator simulators)
random_vectors.h generated cross-validation vectors
gen_random_vectors.py regenerator (uses `cryptography` lib)
vhdl_wrapper/ VHDL-2008 wrapper for mixed-language designs
scripts/ helper scripts (synth_report.sh, vhdl_cosim.sh)
Makefile build/lint/sim/synth/synth_report/vhdl-test
Bug reports and patches welcome. Process:
- File an issue first for non-trivial changes.
- Fork, branch, and run
make lint test-alllocally. - Open a pull request with a description of what changed and why.
- CI runs the full test suite plus
synth_report; both must be green.
@misc{chacha20poly1305-fpga-core,
title = {{chacha20poly1305-fpga-core}: a small dual-licensed ChaCha20-Poly1305 AEAD IP core in SystemVerilog},
author = {Achour, Ayoub},
year = {2026},
howpublished = {\url{https://github.com/ayoub-ac/chacha20poly1305-fpga-core}}
}- IETF RFC 8439, ChaCha20 and Poly1305 for IETF Protocols, May 2018.
- D. J. Bernstein, ChaCha, a variant of Salsa20, 2008.
- D. J. Bernstein, The Poly1305-AES message-authentication code, 2005.
- IETF RFC 7905, ChaCha20-Poly1305 Cipher Suites for Transport Layer Security (TLS), 2016.
Ayoub Achour - github.com/ayoub-ac