Hardware fingerprinting system using an FPGA-based substitution-permutation network (SPN) on the Shrike FPGA board (Renesas SLG47910V + RP2040).
The FPGA implements an 8-round non-linear S-box + permutation network in its LUT fabric. The RP2040 sends 64 deterministic challenges (32-bit each), and the FPGA processes each through the SPN, returning 4 PUF bits per challenge — totaling 256 PUF bits.
Combined with the RP2040's factory-burned flash unique ID, the system produces a SHA-256 fingerprint unique to each board.
┌──────────────────────────────────┐ UART ┌──────────────────────┐
│ Shrike FPGA │ ◄──────────► │ RP2040 │
│ │ (115200 bd) │ │
│ ┌──────────────────────┐ │ │ ┌────────────────┐ │
│ │ SPN PUF Core │ │ │ │ sram_puf.py │ │
│ │ ───────────── │ │ 64 challenges │ │ │ │
│ │ 8 rounds of: │ │◄────────────────┤ │ • test_uart() │ │
│ │ • S-box (AND+XOR) │ │ │ │ • demo() │ │
│ │ • Bit permutation │ │ 256 PUF bits │ │ • enroll() │ │
│ │ (rotate + XOR) │ │────────────────►│ │ • verify() │ │
│ └──────────────────────┘ │ │ └────────────────┘ │
└──────────────────────────────────┘ └──────────────────────┘
| Metric | Value |
|---|---|
| PUF bits | 256 |
| Stability | 100% (256/256 stable across 10 reads) |
| Bit balance | 51.6% ones / 48.4% zeros |
| FPGA utilization | 17.2% (193 LUTs) |
| FPGA Pin | Signal | Dir | RP2040 Pin |
|---|---|---|---|
| 3 | Reset | In | GP2 |
| 4 | UART TX | Out | GP1 (RX) |
| 6 | UART RX | In | GP0 (TX) |
Open Go Configure, synthesize with Yosys, run P&R, generate bitstream. Flash to Shrike board.
Copy firmware/micropython/sram_puf.py to the RP2040 via Thonny.
import sram_puf
sram_puf.test_uart()Expected:
Test 1: BRAM Read (cmd 0x01)
✓ Received 258 bytes
Test 2: PUF Challenge (cmd 0x04)
✓ Response: 0x09 (PUF bits: 1001)
sram_puf.demo()Expected output (non-zero FPGA PUF data, ~50% ones):
FPGA PUF Raw (32 bytes):
45 2e 73 0a 9d fc 9d 55 c3 13 4f 95 3f 9e d4 88
f0 0e 19 ed 1f b9 9d bc 03 08 c6 2b 48 80 f7 51
PUF Fingerprint (32 bytes / 256 bits):
c8 ac 0a 57 d8 77 15 2d 22 db b8 48 fb ff 2d a9
50 f0 78 6f 48 ed a4 97 7c f6 8b 12 0b 1e 0f e4
Total bits: 256 | 1s: 132 (51.6%) | 0s: 124 (48.4%)
sram_puf.enroll(10) # 10 measurement cyclessram_puf.verify() # Expected: ✓ MATCHuart_sum/
├── ffpga/
│ ├── src/
│ │ ├── top.v ← FPGA top module (UART + PUF controller)
│ │ ├── arbiter_puf.v ← SPN PUF core (8-round S-box + permutation)
│ │ ├── uart_rx.v ← UART receiver
│ │ ├── uart_tx.v ← UART transmitter
│ │ ├── BRAM0.v ← Native BRAM module
│ │ └── bram0_bb.v ← BRAM black-box declaration
│ └── build/
│ └── synth_script.ys ← Yosys synthesis script
├── firmware/
│ └── micropython/
│ └── sram_puf.py ← PUF firmware (demo/enroll/verify)
├── PROJECT_REPORT.md ← Full technical report
└── README.md ← This file
| Command | TX (RP2040→FPGA) | RX (FPGA→RP2040) | Description |
|---|---|---|---|
0x01 |
1 byte | 258 bytes | BRAM read (256 data + AA 55 markers) |
0x04 |
5 bytes (cmd + 4-byte challenge) | 1 byte (4 PUF bits) | PUF challenge-response |