mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat: add comprehensive test suite for CRYPTO-P0 functionality
- Introduced for independent mathematical operations and RSA fixtures. - Implemented for testing DMA channels with AES ECB and memory interactions. - Created to validate RSA-2048 operations and Montgomery reduction. - Developed for testing AES and SHA vector compliance. - Added to validate image packing, integer root calculations, and inventory checks. - Ensured robust error handling and validation in tests to catch data corruption and recursive hierarchy issues.
This commit is contained in:
16 files changed
+2183
-21
No files matched your search
@@ -421,6 +421,16 @@ endif
|
||||
include physical/librelane/Makefile
|
||||
include physical/ecc/Makefile
|
||||
|
||||
.PHONY: crypto-p0-constants crypto-p0-rtl crypto-p0-baseline crypto-p0-report
|
||||
crypto-p0-constants: manifest
|
||||
$(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py constants --variant-root $(VARIANT_ROOT)
|
||||
crypto-p0-rtl: manifest
|
||||
$(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py rtl --variant-root $(VARIANT_ROOT) --jobs $(JOBS)
|
||||
crypto-p0-baseline: manifest
|
||||
$(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py synth --variant-root $(VARIANT_ROOT)
|
||||
crypto-p0-report: manifest
|
||||
$(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py report --variant-root $(VARIANT_ROOT)
|
||||
|
||||
.PHONY: help config doctor setup setup-regression setup-mpw setup-vexiiriscv setup-clusterip setup-ip setup-pdk setup-app setup-apu-reference setup-apu-kws-reference apu-p5-bundle apu-p5-corpus apu-p7-model apu-p9-coefficients apu-p9-evidence apu-p9-memory-ab setup-hp-linux hp-linux hp-bundle hp-linux-sim hp-smoke-bundle hp-smoke-sim hp-apu-bundle hp-apu-sim \
|
||||
clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \
|
||||
regress-smoke regress-rtl regress-pr regress-nightly sim-asm format format-check sw-format sw-format-check mk-format \
|
||||
@@ -437,6 +447,10 @@ include physical/ecc/Makefile
|
||||
help:
|
||||
@printf '%s\n' \
|
||||
'retroSoC build targets:' \
|
||||
' crypto-p0-constants verify and package the independent CRYC1 baseline' \
|
||||
' crypto-p0-rtl run V1 Crypto, RSA-2048 and real DMA evidence' \
|
||||
' crypto-p0-baseline run unchanged balanced Crypto block synthesis' \
|
||||
' crypto-p0-report check and aggregate CRYPTO-P0 evidence' \
|
||||
' firmware | asm build firmware' \
|
||||
' comp | sim behavioral simulation' \
|
||||
' sim-asm build/run the assembly self-test' \
|
||||
|
||||
+4
-1
@@ -44,7 +44,10 @@ the root README and subsystem guides.
|
||||
register ABI, SDK API, scheduling, error, IRQ, and stream contracts.
|
||||
- [ip/crypto.md](ip/crypto.md) records the commercial AES/SHA/RSA survey and
|
||||
defines the Mini SoC crypto architecture, APB/DMA ABI, security boundary,
|
||||
HAL, verification evidence, and commercialization roadmap.
|
||||
HAL, verification evidence, and commercialization roadmap. Its approved
|
||||
storage refreeze specifies six private SRAM banks, LP-loaded/locked constants,
|
||||
synchronous AES/SHA/RSA access, APB V2 and verified scrub completion;
|
||||
CRYPTO-P0 through CRYPTO-P2 remain implementation/qualification work.
|
||||
- [ip/usb2.md](ip/usb2.md) defines the ULPI USB 2.0 dual-role architecture,
|
||||
AXI4 descriptor DMA, APB4 ABI, dedicated pads, HAL, and release gates.
|
||||
- [ip/jpeg.md](ip/jpeg.md) records the commercial JPEG IP/SoC survey and
|
||||
|
||||
@@ -19,6 +19,10 @@ extension interfaces.
|
||||
- [i2s.md](i2s.md) defines the APB4 I2S master transceiver.
|
||||
- [usb2.md](usb2.md) defines the dual-role ULPI USB 2.0 controller, AXI4
|
||||
descriptor DMA, dedicated-pad integration, HAL, and commercial release gates.
|
||||
- [crypto.md](crypto.md) defines AES/SHA-2/RSA and its macro-first refreeze:
|
||||
six private SRAM banks, LP constant initialization, APB V2, bounded erasure,
|
||||
preserved algorithm/DMA/IRQ contracts and stable CRYPTO-P0/P1/P2 phases.
|
||||
The storage refreeze is specified, not yet implemented or qualified.
|
||||
- [jpeg.md](jpeg.md) defines the Baseline JPEG encoder/decoder, commercial
|
||||
reference survey, APB4/AXI4 DMA and SG-ring ABI, HAL, verification evidence,
|
||||
measured performance baseline, and 1080p60 optimization gates.
|
||||
|
||||
+571
-20
@@ -8,9 +8,15 @@ RSA-2048 modular exponentiation, a combined interrupt, PIO access, and central
|
||||
DMA streaming. The implementation consistently uses `RSA`, not the common
|
||||
`RAS` transposition.
|
||||
|
||||
This document records the commercial reference survey, freezes the current
|
||||
hardware/software contract, and separates implemented behavior from the
|
||||
commercialization roadmap. The executable sources of truth are:
|
||||
This document records the commercial reference survey and the 2026-09-25
|
||||
macro-first storage refreeze. The refreeze is an approved implementation
|
||||
requirement, not a claim that the current RTL already contains Crypto SRAM
|
||||
macros. Revision `92830d963da2f1e39bb219c0d377bc4889c07755` is the pre-refreeze
|
||||
baseline. The V2 storage, initialization, cycle and zeroization requirements
|
||||
below supersede the corresponding V1 implementation behavior. Algorithms,
|
||||
numerical results and existing system allocations remain fixed.
|
||||
|
||||
The executable sources of truth for the baseline are:
|
||||
|
||||
- `rtl/ip/security/crypto_*.sv`, `apb4_crypto.sv`, and `crypto_define.svh`;
|
||||
- `crt/include/retrosoc/hal/crypto_regs.h`, `crypto.h`, and
|
||||
@@ -76,17 +82,21 @@ central DMA. AES and SHA can operate concurrently through PIO; they arbitrate
|
||||
the one crypto DMA input, and only AES produces the crypto DMA output stream.
|
||||
RSA has a private 32-bit-limb datapath and does not occupy the stream path.
|
||||
|
||||
The production Mini instance expands the DMA from four to six channels.
|
||||
Channels 4 and 5 are reserved for crypto input and output so a bidirectional
|
||||
AES transaction cannot deadlock by competing for the legacy bulk channel.
|
||||
The reusable DMA module default remains four channels for existing standalone
|
||||
users.
|
||||
The production Mini instance currently has eight DMA channels. Channels 4
|
||||
and 5 remain reserved for crypto input and output so a bidirectional AES
|
||||
transaction cannot deadlock by competing for the legacy bulk channel. The
|
||||
storage refreeze does not change the channel count or ownership; see
|
||||
[DMA V2](dma.md).
|
||||
|
||||
The controller is management-only. The address map denies the user core any
|
||||
access, the aggregate peripheral interrupt is group bit 18/core IRQ23, and the
|
||||
DMA aggregate remains IRQ20.
|
||||
|
||||
## Implemented MVP
|
||||
## V1 Implemented Baseline
|
||||
|
||||
This section describes the pre-refreeze RTL. V2 keeps the listed algorithms
|
||||
and transfer capacities, but replaces table/operand storage, initialization,
|
||||
round timing and erasure completion as specified below.
|
||||
|
||||
| Area | Implemented behavior | Deliberate limit |
|
||||
| --- | --- | --- |
|
||||
@@ -146,7 +156,7 @@ the DMA API deliberately rejects partial words. Hardware accepts SHA DMA input,
|
||||
but a public SHA DMA HAL call is deferred until timeout ownership and digest
|
||||
completion semantics are covered end to end.
|
||||
|
||||
## APB4 Register ABI
|
||||
## Existing APB4 Register ABI
|
||||
|
||||
All registers are 32-bit and require word-aligned accesses. Multiword arrays
|
||||
advance by four bytes. `RO/W1C` means read current sticky state and write one to
|
||||
@@ -155,7 +165,7 @@ clear.
|
||||
| Offset | Register | Access | Purpose |
|
||||
| ---: | --- | --- | --- |
|
||||
| `0x000` | `IP_ID` | RO | `0x43525950` (`CRYP`) |
|
||||
| `0x004` | `IP_VERSION` | RO | `0x00010000` |
|
||||
| `0x004` | `IP_VERSION` | RO | V1 baseline `0x00010000`; V2 refreeze `0x00020000` |
|
||||
| `0x008` | `CAPABILITY0` | RO | Algorithm, DMA, control, and security feature bits |
|
||||
| `0x00c` | `CAPABILITY1` | RO | RSA bits `[31:16]`, DMA width `[15:8]`, APB width `[7:0]` |
|
||||
| `0x010` | `COMMAND` | WO | bit 0 zeroize; bits 1..3 abort AES/SHA/RSA |
|
||||
@@ -205,13 +215,551 @@ Bits 16..23 describe PIO, interrupt, zeroize, secret-read protection,
|
||||
concurrent engines, counters, APB errors, and central DMA. Unknown capability
|
||||
bits must be ignored by software.
|
||||
|
||||
## V2 Macro-first Storage Contract
|
||||
|
||||
### Requirements and non-goals
|
||||
|
||||
The feature slug remains `crypto`. These requirements replace storage and
|
||||
latency, not AES/SHA/RSA algorithms. The first physical acceptance profile is
|
||||
`configs/ci/ihp130.mk`, with `HAVE_SRAM_MACRO=YES` and `PDK_BEHAV=NO` for
|
||||
synthesis. Existing non-macro profiles retain functional compatibility through
|
||||
synchronous behavioral RAMs with the same initialization and transactions.
|
||||
|
||||
| Requirement | Normative contract |
|
||||
| --- | --- |
|
||||
| `CRYPTO-SRAM-001` | Macro builds MUST replace all indexed algorithm ROMs, AES expanded-key RAM, SHA schedule RAM and Montgomery temporary RAM with physical SRAM wrappers. No runtime S-box, inverse S-box, Rcon or SHA K lookup may remain as an inferred ROM, giant case/mux table or register copy. |
|
||||
| `CRYPTO-SRAM-002` | RSA staged operands, retained Montgomery values, exponent/window tables, preparation workspace and results MUST use limb-addressed SRAM. Moving unpacked arrays to packed vectors is not an acceptable memory reduction. |
|
||||
| `CRYPTO-SRAM-003` | Preserve AES-128/192/256 encrypt/decrypt ECB/CBC/CTR, partial CTR tails, SHA-224/256 padding, RSA-2048 preparation/public/private operations, result verification, capacities, byte order and exact outputs. |
|
||||
| `CRYPTO-SRAM-004` | Preserve AES/SHA PIO concurrency and RSA independence, the existing 32-bit stream subset, central-DMA channels 4/5 and requests 12/13, management-only access, APB base and LP IRQ23. No private AXI master, descriptor ABI, resource-controller slot, new pad, clock or CDC is allocated. |
|
||||
| `CRYPTO-SRAM-005` | Use synchronous, one-port-per-bank reads/writes and bounded microsteps. The old AES/SHA one-round-per-cycle guarantee is superseded by the cycle limits below. No memory multi-pumping, falling-edge access or software crypto fallback. |
|
||||
| `CRYPTO-SRAM-006` | Constants MUST be initialized through the public LP interface, validated by SRAM readback and locked before any engine starts. No synthesizable initial/readmemh preload or hardware ROM fallback. |
|
||||
| `CRYPTO-SRAM-007` | Reset, zeroize, abort and faults MUST revoke valid state and prevent stale results. Physical clearing MUST finish before successful erasure acknowledgement. Private-RSA scheduling MUST remain independent of exponent value. |
|
||||
| `CRYPTO-SRAM-008` | Only the three existing bounded stream FIFOs may remain inferred memories: AES input/output 8x37 each and SHA input 16x37, 1184 bits total. These FIFOs are an explicit exception, not newly macro-backed storage. |
|
||||
| `CRYPTO-SRAM-009` | Qualify macro counts, inferred storage, remaining register bits, cycle bounds, synthesis time, peak RSS and STA using like-for-like artifacts; do not infer a speedup from the bit count. |
|
||||
|
||||
Fixed-width arithmetic constants, the active AES 128-bit round state, SHA's
|
||||
eight 32-bit working words, scalar carries, counters, FSM state, short operand
|
||||
and response registers may remain in flops. They MUST be individually reported
|
||||
with widths and purpose. This allowance cannot retain an AES key-schedule copy,
|
||||
SHA schedule array, 2048-bit RSA operand/result snapshot or predecoded table.
|
||||
Preserve the FIFO depths; widening or adding inferred FIFOs needs another
|
||||
reviewed contract. Software tables in the boot image and verification oracles
|
||||
are permitted and are not hardware ROM implementations.
|
||||
|
||||
AEAD/HMAC, new key sizes, CRT, masking/blinding, HUK/key ladders, Linux crypto
|
||||
drivers, private DMA, ECC/MBIST/secure scan and side-channel certification remain
|
||||
deferred. Vexii and APU storage are outside this refreeze.
|
||||
|
||||
### Physical layout and implementation boundary
|
||||
|
||||
Instantiate exactly six dedicated `tc_sram_1024x32` wrappers (24 KiB physical
|
||||
capacity). Each resolves to one `RM_IHPSG13_1P_1024x32_c2_bm_bist` in the locked
|
||||
IHP130 PDK. Reuse `rtl/tech/tc_sram.sv`, its masks and model semantics; do not
|
||||
edit the PDK or managed Common. GF180/SKY130 use the existing wrapper mappings;
|
||||
their physical-cell counts differ and require separate qualification. With
|
||||
`HAVE_SRAM_MACRO=NO`, infer the same six synchronous banks explicitly, without
|
||||
claiming ASIC memory reduction. An enabled but unavailable macro mapping MUST
|
||||
fail elaboration rather than silently becoming flops or an undriven cell.
|
||||
|
||||
All addresses in the following tables are 32-bit word rows, inclusive. Unused
|
||||
rows are reserved, initialized/cleared as specified and never aliased to another
|
||||
engine. This capacity is private storage, not a new software-addressable SRAM
|
||||
window or a reduction of existing algorithm limits.
|
||||
|
||||
| Bank | Owner | Fixed allocation |
|
||||
| ---: | --- | --- |
|
||||
| 0 | AES constants | Rows 0..255 forward S-box; 256..511 inverse S-box; 512..527 Rcon[0..15]; values in bits 7:0, upper bits zero; 528..1023 zero padding. |
|
||||
| 1 | SHA constants | Rows 0..63 K[0..63]; 64..71 SHA-224 initial state; 72..79 SHA-256 initial state; 80..1023 zero padding. |
|
||||
| 2 | AES mutable | 0..7 staged key; 16..75 expanded key; 80..83 IV; 84..87 chain/counter; 88..91 input block; 92..95 output block; all other rows reserved zero. |
|
||||
| 3 | SHA mutable | 0..15 schedule ring; 16..31 block/padding buffer; 32..39 chaining state; 40..47 compression initial state; 48..55 digest; all other rows reserved zero. |
|
||||
| 4 | RSA retained | Sixteen 64-word slots, detailed below. |
|
||||
| 5 | RSA arithmetic | 0..63 Montgomery left; 64..127 right; 128..191 modulus; 192..256 accumulator (65 limbs); 257..320 subtracted candidate; 321..384 product; 385..449 preparation double (65 limbs); 450..513 preparation subtraction; remaining rows reserved zero. |
|
||||
|
||||
RSA bank 4 slot `s` occupies rows `64*s..64*s+63`, least-significant limb
|
||||
first. Slots 0..13 are, respectively: modulus, exponent, base, released result,
|
||||
R-squared, Montgomery one, Montgomery base, running Montgomery result, window
|
||||
table 2, window table 3, private candidate, verification Montgomery base,
|
||||
preparation modulus, and copy workspace. Slots 14..15 are reserved zero.
|
||||
Upper unused bits of a 65th limb MUST be cleared. Explicit SRAM-to-SRAM copies
|
||||
are allowed; full-vector shadow registers are not.
|
||||
|
||||
AES staged-key/IV words retain APB little-endian byte lanes. Internal expanded
|
||||
key and round/block words use the baseline FIPS byte ordering; conversion occurs
|
||||
at the engine boundary, not by changing the register ABI. SHA schedule/block
|
||||
words represent big-endian message words, and SHA state/digest slots keep the
|
||||
existing most-significant-word-first convention. Test these conversions
|
||||
independently of the image generator.
|
||||
|
||||
The owned implementation boundary is a private bank store plus initialization/
|
||||
scrub controller under `apb4_crypto`, with independent AES, SHA and RSA clients.
|
||||
The RSA core and Montgomery interface become limb transactions; their existing
|
||||
wide internal ports are not a software ABI. Use Common register/handshake
|
||||
primitives where their reset/enable semantics fit. Each bank accepts at most
|
||||
one access per PCLK edge; responses are registered and tagged with the owning
|
||||
operation epoch. A client advances only after retirement, holds an unaccepted
|
||||
request stable and never consumes the macro output during a write.
|
||||
|
||||
AES and SHA have separate constant and mutable banks and need no mutual
|
||||
arbitration. RSA serializes accesses within its own two banks. Scalar response
|
||||
holding registers handle backpressure without rereading a mutable address.
|
||||
No read-during-write behavior is assumed, and no extra unbounded queue is
|
||||
introduced. Legal APB access owns the corresponding idle engine bank until
|
||||
retirement; operating engines retain the existing busy-time write restrictions.
|
||||
Reset/scrub has priority over all ordinary traffic in the banks it owns.
|
||||
|
||||
### Constant image and boot initialization
|
||||
|
||||
The `CRYC1` layout identifier is `0x43525901`. Its payload is exactly 8192
|
||||
bytes: all 1024 little-endian words of bank 0, followed by all 1024 words of
|
||||
bank 1, with no header. S-boxes and SHA constants equal the baseline algorithms.
|
||||
Rcon rows contain zero at indices 0 and 15, and successive AES xtime powers
|
||||
starting with 1 for indices 1..14, matching the baseline helper. SHA initial
|
||||
states use the standard most-significant digest-word-first ordering within
|
||||
their eight-word ranges.
|
||||
|
||||
Freeze-time parsing of the baseline tables, inverse-S-box consistency checking
|
||||
and independent packing produced these identities (artifact identity only,
|
||||
not synthesized-hardware evidence):
|
||||
|
||||
| Identity | Value |
|
||||
| --- | --- |
|
||||
| Payload bytes / words | 8192 / 2048 |
|
||||
| CRC32/ISO-HDLC | `0x99ca52fe` |
|
||||
| SHA-256 | `b662a0729bf85789aa9c9bd4a9f8a13ebe44e296deebda486bef2792b1e9d47f` |
|
||||
|
||||
The CRC uses reflected polynomial `0xedb88320`, initial/final XOR
|
||||
`0xffffffff`, and low byte first per word. Hardware checks the fixed CRC,
|
||||
not a caller-supplied expected checksum. CRC detects corruption; it does not
|
||||
authenticate compromised firmware or provide runtime SRAM fault protection.
|
||||
The trusted LP boot image owns initialization. A deterministic host generator
|
||||
and a separately implemented algorithmic oracle MUST agree on every S-box,
|
||||
inverse, Rcon and SHA word and on all zero padding. Keep the old RTL lookup
|
||||
functions only in verification inputs once their product consumers migrate.
|
||||
The freeze-time identities were also reproduced from GF(2^8) inversion/affine
|
||||
AES construction and prime-root SHA constants without reading the RTL tables.
|
||||
|
||||
After reset, revoke all valid/lock state and scrub mutable storage before
|
||||
accepting initialization. LP issues `MEM_CONTROL.BEGIN`, then exactly 2048
|
||||
full-word `TABLE_DATA` writes in increasing row order. Hardware owns the write
|
||||
index; software cannot address mutable banks through the loader. Each accepted
|
||||
write increments `TABLE_WORDS` exactly once. Reserved/padding words and the
|
||||
upper bits of byte-valued rows MUST be zero. `COMMIT` requires exactly 2048
|
||||
words, checks the input CRC, then reads all 2048 physical SRAM words back and
|
||||
checks their CRC and padding before atomically asserting `TABLE_VALID`,
|
||||
`TABLE_LOCKED` and `READY`. Failed or short loads leave engines disabled;
|
||||
overlong writes fail without changing any bank.
|
||||
|
||||
`BEGIN` is allowed only after reset scrub, with every engine idle, no active
|
||||
loader/verify, no erasure fault and no table lock. It restarts a failed load at
|
||||
word zero. `CANCEL` cancels an unlocked load/verify, drains its pending access
|
||||
and leaves constants invalid. No per-word readback register is exposed. After
|
||||
locking, only hard/PCLK reset unlocks the tables. Zeroize clears mutable state
|
||||
but preserves a previously validated constant image. No Crypto work may start
|
||||
without `READY`; legacy firmware that omits initialization fails closed.
|
||||
|
||||
A sequence/count, padding or CRC error during an unlocked initialization ends
|
||||
that attempt, drains its pending request, clears LOAD_ACTIVE/VERIFY_BUSY and
|
||||
leaves TABLE_VALID/TABLE_LOCKED/READY clear. A rejected write after lock does
|
||||
not unlock or modify the validated image. BEGIN clears earlier recoverable
|
||||
MEM_ERROR and ERROR_STATUS.MEMORY, but does not acknowledge sticky IRQs or
|
||||
unrelated ACCESS errors. CANCEL during idle/unlocked state is an idempotent
|
||||
no-op; during locked state it fails. A fatal maintenance fault ends active
|
||||
maintenance, keeps FAULT latched and never asserts READY or ZEROIZED.
|
||||
|
||||
Host generation emits an 8192-byte image and manifest under the selected build
|
||||
variant and a firmware include consumed through the normal software build.
|
||||
There is no external download, new dependency or filesystem requirement on the
|
||||
freestanding target. LP startup and every standalone test initialize through
|
||||
the public interface; hierarchical SRAM preload is not acceptance evidence.
|
||||
|
||||
### APB V2 discovery and lifecycle ABI
|
||||
|
||||
V2 deliberately advertises `IP_VERSION=0x00020000`: mandatory initialization
|
||||
and asynchronous erase completion are not binary-compatible V1 behavior.
|
||||
Existing offsets, algorithm capability bits 0..23 and `CAPABILITY1` remain.
|
||||
`CAPABILITY0[24]` advertises this storage/initialization contract; bit 25
|
||||
advertises configured macro backing and is clear for the inferred fallback.
|
||||
Bits 26..31 remain zero. Do not advertise V2 on a partially migrated product.
|
||||
|
||||
| Offset | Register | Access / reset | V2 semantics |
|
||||
| ---: | --- | --- | --- |
|
||||
| `0x028` | `MEM_STATUS` | RO / `0x00000004` | READY bit 0; TABLE_VALID 1; SCRUB_BUSY 2; LOAD_ACTIVE 3; VERIFY_BUSY 4; TABLE_LOCKED 5; FAULT 6. READY requires valid locked constants, completed erasure and no fault. |
|
||||
| `0x02c` | `MEM_CONTROL` | WO / 0 | BEGIN bit 0, COMMIT bit 1, CANCEL bit 2; one-hot commands only; zero is a no-op. |
|
||||
| `0x030` | `TABLE_ID` | RO / `0x43525901` | Frozen CRYC1 identity. |
|
||||
| `0x034` | `TABLE_WORDS` | RO / 0 | Accepted payload count, 0..2048; BEGIN/CANCEL/reset clear it. |
|
||||
| `0x038` | `TABLE_DATA` | WO / 0 | Auto-incrementing loader input; full `PSTRB=0xf` required; reads fail. |
|
||||
| `0x03c` | `TABLE_CRC` | RO / 0 | Last completed full SRAM-readback CRC; zero until a complete readback, including after BEGIN/CANCEL/reset. |
|
||||
| `0x040` | `MEM_ERROR` | RO / 0 | First lifecycle error code bits 7:0; physical bank bits 10:8; row bits 20:11; bit 31 valid; other bits zero. Non-addressed errors use bank/row zero. |
|
||||
| `0x044` | `MEM_CYCLES` | RO / 0 | Elapsed internal verify/scrub cycles; reset at each maintenance operation and retained on completion/fault. Counts active PCLK edges, not LP polling or loader inter-write gaps. |
|
||||
|
||||
`STATUS[3]` is maintenance busy (scrub/load/verify), and bit 4 is READY; reset
|
||||
STATUS therefore has bit 3 set. Existing busy bits describe their respective
|
||||
engine including abort cleanup. During global maintenance, engine
|
||||
result/key/prepared valids and PIO input-ready are clear. Existing global
|
||||
status, identity, IRQ and error registers remain accessible.
|
||||
|
||||
`IRQ_STATE/ENABLE/TEST[5]` is the new memory-ready event on successful COMMIT;
|
||||
bits 0..4 retain their assignments. `ZEROIZED` bit 4 reports explicit global
|
||||
zeroize completion only, never command acceptance or reset scrub. Clear stale
|
||||
ZEROIZED on accepted zeroize; an IRQ_TEST bit cannot serve as physical erasure
|
||||
evidence. Concurrent events win over W1C. `ERROR_STATUS[4]` is MEMORY; W1C
|
||||
clears its diagnostic and MEM_ERROR together only when maintenance is idle.
|
||||
W1C never restores readiness or clears a fatal erasure fault. Errors also set
|
||||
the existing error IRQ. MEM_ERROR codes are: 1 sequence/count, 2 padding,
|
||||
3 input CRC, 4 SRAM readback CRC, 5 scrub verification, 6 internal progress
|
||||
timeout. Codes 5/6 latch FAULT until hard reset; codes 1..4 allow BEGIN retry.
|
||||
Invalid APB encodings additionally retain `ERROR_STATUS.ACCESS` behavior.
|
||||
|
||||
New command/data registers require full-word writes; reserved command bits,
|
||||
multiple command bits and invalid accesses fail with `PSLVERR` without side
|
||||
effects. Existing legal byte strobes retain their meaning. Bank-backed APB
|
||||
reads/writes may insert at most four access-phase wait cycles; data and response
|
||||
remain stable through the completing handshake. Side effects occur exactly
|
||||
once. Invalid/secret/busy-time accesses fail promptly rather than wait for a
|
||||
long crypto operation. Never acknowledge a write before its SRAM retirement,
|
||||
return an unregistered SRAM read or expose a raw secret-bank address.
|
||||
|
||||
During scrub or load/verify, engine data/key/operand/configuration accesses fail
|
||||
with `PSLVERR`; only global identity/status/error/IRQ and the legal loader or
|
||||
zeroize controls operate. Invalid result/digest reads return zero; secret reads
|
||||
always fail. Mutable-window reads during the owning engine's computation fail
|
||||
with `PSLVERR`, allowing its private schedule to remain independent of APB
|
||||
polling. Existing legal idle operand access is preserved.
|
||||
|
||||
### Reset, zeroize, abort and stream ownership
|
||||
|
||||
All banks, engines, APB and internal streams remain in the existing PCLK domain
|
||||
and use its active-low reset. No additional CDC/RDC path is allocated. SRAM
|
||||
contents themselves do not have a reset pin. On reset, clear scalar sensitive
|
||||
registers, revoke keys/results/prepared state/table lock, suppress engine work
|
||||
and start a write-zero/readback-zero sweep of every row of banks 2..5. These
|
||||
four banks can scrub in parallel. Boot scrub MUST complete within 2056 active
|
||||
PCLK cycles after reset release; a new reset restarts from row zero. Constants
|
||||
remain inaccessible until completely overwritten and validated.
|
||||
|
||||
An accepted global zeroize cancels unlocked initialization, invalidates results
|
||||
and secrets immediately, clears sensitive scalar/round state and starts the
|
||||
same full mutable-bank sweep. It physically clears the payload of all three
|
||||
FIFO exceptions as well as their pointers; flushing only valid bits is not
|
||||
erasure. Reuse the Common FIFO through an owned clear controller that fills
|
||||
and drains every slot with zero while application ports are isolated; do not
|
||||
patch managed FIFO storage or erase it by hierarchical access. Check the
|
||||
cleared payloads before restoring the empty state. Reads of each scrubbed
|
||||
SRAM row must return zero before completion.
|
||||
All banks include reserved rows in the sweep. ZEROIZED is emitted only after
|
||||
the last write/read response drains and scalar/FIFO erasure also completes;
|
||||
the bound is 2056 active cycles from accepted command. Repeated zeroize while
|
||||
scrubbing is idempotent, does not restart the sweep or create an early event.
|
||||
Hard reset and accepted zeroize take precedence over engine DONE/START.
|
||||
|
||||
To preserve the AXI4-Stream stability contract, a V2 zeroize request while an
|
||||
AES/SHA DMA operation is active or an output beat is held valid MUST fail
|
||||
promptly with `PSLVERR` and ACCESS; it is not accepted as an erase request.
|
||||
Software first completes or quiesces the owning DMA transfer and confirms
|
||||
that no beat remains held. Do not drop TVALID or erase its payload underneath
|
||||
backpressure. PIO operations and private RSA may be interrupted by accepted
|
||||
zeroize without waiting for their calculation to finish. Reset remains the
|
||||
coordinated emergency recovery boundary, not evidence of immediate physical
|
||||
erasure while PCLK is stopped.
|
||||
|
||||
Per-engine abort revokes that engine's result, stops new input/work and drains
|
||||
any already-presented output beat before scrubbing its mutable banks/FIFO and
|
||||
clearing BUSY. Do not assert DONE for abort. An outstanding stream beat can
|
||||
extend this drain indefinitely under external backpressure; count/report this
|
||||
separately from the bounded local scrub. Merely aborting the DMA consumer is
|
||||
not evidence that a held producer beat retired. The HAL must preserve/drain
|
||||
that transfer or require coordinated reset on timeout. AES abort preserves
|
||||
staged/expanded keys in rows 0..75; erases rows 76..1023 and all transient round
|
||||
state/FIFOs. A key expansion interrupted by AES abort leaves KEY_VALID clear;
|
||||
only a previously completed key schedule may retain KEY_VALID. SHA abort clears
|
||||
bank 3 and its scalar state/FIFO. RSA abort clears
|
||||
banks 4/5 and revokes PREPARED; modulus preparation must be repeated. Global
|
||||
zeroize always erases the AES key region as well.
|
||||
|
||||
SCRUB_BUSY and MEM_CYCLES describe global reset/zeroize maintenance, not a
|
||||
per-engine abort. Local abort cleanup retains that engine's BUSY and cycle
|
||||
counter, completes its scrub within 2056 cycles after the last pending stream
|
||||
beat retires, and does not clear another engine's valid state or block its
|
||||
banks. A global zeroize supersedes any local cleanup. Local scrub failure
|
||||
latches the same fatal MEMORY fault, revokes global READY and invalidates all
|
||||
engine results. New starts while the owning engine is still cleaning fail.
|
||||
|
||||
No clear loop, return-early branch, table selection or SRAM arbitration during
|
||||
private RSA may depend on a secret exponent bit. Retain all 1024 two-bit windows,
|
||||
two squarings plus one multiply per window and the mandatory 65537 recheck.
|
||||
Select window operands by scanning all four candidates in a fixed address
|
||||
order and selecting in scalar logic, rather than exposing secret-indexed bank
|
||||
timing. Do not publish the candidate until verification succeeds. This is a
|
||||
logical scheduling requirement, not a new DPA/FIA resistance claim.
|
||||
|
||||
### Execution cycles and HAL
|
||||
|
||||
Replace combinational lookup fanout with a single AES table read lane and a
|
||||
single SHA constant read lane. AES processes SubBytes in fixed byte order and
|
||||
fetches round keys by words; SHA fetches schedule operands and K through
|
||||
bounded microsteps. RSA retains a 32-bit-limb Montgomery datapath and sequences
|
||||
SRAM reads, arithmetic and writes. Small pipeline cuts are permitted without
|
||||
changing arithmetic widths or numerical results.
|
||||
|
||||
The following are acceptance ceilings in active PCLK cycles, not measured
|
||||
performance. They include private-bank waits and arithmetic control; they
|
||||
exclude external input starvation/output backpressure and LP initialization
|
||||
write gaps. There is no inter-engine bank contention to exclude.
|
||||
|
||||
| Operation | Maximum cycles |
|
||||
| --- | ---: |
|
||||
| Full reset/global-zeroize scrub, including readback | 2056 |
|
||||
| COMMIT validation after last accepted payload word | 8192 |
|
||||
| AES key expansion, any supported key size | 2048 |
|
||||
| AES block, including mode bookkeeping, with key already valid | 1024 |
|
||||
| SHA compression block, including schedule and local state transfer | 2048 |
|
||||
| Montgomery multiply, including operand/result SRAM copies | 131072 |
|
||||
| RSA modulus preparation | 4194304 |
|
||||
| RSA private operation, including table preparation and public recheck | 536870912 |
|
||||
| RSA public operation, all supported exponent lengths/values | 1073741824 |
|
||||
|
||||
AES/SHA streaming overhead beyond the per-block bounds is at most 128 local
|
||||
cycles per operation; SHA includes every compression block added by padding.
|
||||
Existing operation counters include local SRAM waits and external stalls, and
|
||||
retain their 32-bit wrap behavior; verification records unwrapped 64-bit cycle
|
||||
totals. MEM_CYCLES has no externally stalled phase and uses a watchdog equal
|
||||
to the applicable maintenance ceiling. A missed ceiling sets code 6, revokes
|
||||
READY and requires reset; it must not produce a success event. Physical timing
|
||||
is separately checked at the committed PCLK constraint, with 72 MHz Crypto
|
||||
block characterization retained as a separate target, not the reset frequency.
|
||||
|
||||
Keep existing public algorithm function signatures. Add:
|
||||
|
||||
```c
|
||||
rs_status_t rs_crypto_init(rs_timeout_t timeout);
|
||||
rs_status_t rs_crypto_zeroize_wait(rs_timeout_t timeout);
|
||||
```
|
||||
|
||||
`rs_crypto_init` identifies V2/CRYC1, waits for boot scrub, loads the frozen
|
||||
public image, commits and waits for valid+locked+ready. It is idempotent when
|
||||
already ready, returns RS_EIO for live engine/foreign loader ownership or
|
||||
maintenance fault, and RS_ENOTSUP for unsupported hardware/layout. It never
|
||||
zeroizes an active context to obtain ownership. The LP application calls it
|
||||
before the first crypto operation; selftest does so explicitly. It must not
|
||||
require completion of a crypto operation to initialize Crypto.
|
||||
|
||||
Algorithm APIs on V2 check READY before accepting work and return RS_EIO if
|
||||
uninitialized; there is no implicit software crypto or hidden table fallback.
|
||||
`rs_crypto_zeroize_wait` validates the stream-quiescence precondition, clears
|
||||
stale ZEROIZED, submits the command and waits for SCRUB_BUSY=0, no FAULT and a
|
||||
fresh completion. It returns RS_ETIMEOUT without claiming erasure when polling
|
||||
expires. `rs_crypto_zeroize(void)` remains a compatibility wrapper using
|
||||
RS_TIMEOUT_DEFAULT and MUST also wait for physical completion. Use the
|
||||
correct AES_KEY_STATUS bit definition in that path; the baseline mixes it
|
||||
with the distinct AES_STATUS key-valid mask. The updated HAL may retain V1
|
||||
algorithm/zeroize support by explicit IP_VERSION dispatch; V1 must not be
|
||||
reported as satisfying this SRAM contract. Timeout budgets are bounded polls,
|
||||
not claimed PCLK cycles. Timeout never frees live DMA buffers implicitly.
|
||||
Initialization and erasure each consume one monotonically decreasing caller
|
||||
budget across their waits/transfers; do not restart it per word or retry.
|
||||
On initialization timeout, cancel an unlocked attempt and report timeout;
|
||||
never unlock a completed image or claim a still-running scrub completed.
|
||||
LP software serializes lifecycle calls and IRQ_TEST use with operation setup;
|
||||
there is no hidden multi-client initialization or erasure owner.
|
||||
|
||||
Maintain all new offsets, fields, masks and public constants manually in
|
||||
`crypto_define.svh` and `crypto_regs.h`, extend their parity test, and add host
|
||||
tests for pure validation/error handling. No new MISRA deviation is authorized.
|
||||
|
||||
### Verification and synthesis acceptance
|
||||
|
||||
| Evidence | Required coverage |
|
||||
| --- | --- |
|
||||
| Constant identity | Every forward/inverse byte, Rcon and SHA word against independent oracles; exact image hash, CRC, packing and zero padding; generate twice reproducibly. |
|
||||
| Storage primitives | All six banks, first/last rows, byte masks, same-bank collisions, response backpressure, operation epochs and no write-cycle read consumption; behavioral fallback and real IHP FUNCTIONAL models. |
|
||||
| Initialization | Boot scrub, no-init START rejection, short/long/partial-strobe loads, bad padding/CRC, injected SRAM readback corruption, cancel/retry, lock rejection, reset at every load/verify boundary. |
|
||||
| Algorithms | Existing AES key sizes/modes/directions and CTR tails; SHA empty, 55/56/63/64/65-byte and multiblock padding; full RSA-2048 prepare/public/private/recheck and Montgomery carries/edge limbs. Reduced-width RSA tests supplement but cannot replace 2048-bit evidence. |
|
||||
| Concurrency/protocol | Simultaneous AES/SHA PIO and RSA, DMA duplex/backpressure/error, APB wait-state side effects, busy/secret access faults, IRQ/W1C races, unchanged central-DMA reservations and access denial. |
|
||||
| Erasure/security | Abort/reset/zeroize at every state, FIFO payload inspection after scrub, partial/restarted scrub, late responses, stalled output rejection/drain, failure injection, no early ZEROIZED/READY, no stale key/result and equal private-operation cycles for different exponents. |
|
||||
| Formal | Single bank owner/access, stable pending requests/responses, no secret readback, no operation before lock/ready, result release only after private verify, and bounded scrub/commit progress under running clocks and functioning macro assumptions. |
|
||||
| Firmware | Public initialization, selftest, PIO and AES DMA, zeroize completion and timeout recovery through LP; no direct SRAM preload or bypass of macro/model interfaces. |
|
||||
|
||||
Use revision-qualified baseline A at `92830d963da2f1e39bb219c0d377bc4889c07755`
|
||||
and candidate B with identical committed profile, macros, feature enables,
|
||||
locked tools/PDK, hierarchy policy, ABC recipe, timing target and host limits.
|
||||
The 2026-09-25 interrupted full-chip run measured Crypto **91712 bits** at
|
||||
the post-`proc` initial checkpoint: 83968 ROM and 7744 RAM, including the 1184
|
||||
FIFO bits. It also measured 504050 whole-chip inferred bits. Its artifact is
|
||||
`build/ihp130-2026-09-25-09-01-9903b3d112b7/syn/yosys/rpt/retrosoc_asic_initial.rpt`;
|
||||
the manifest records the baseline revision. The run was intentionally stopped
|
||||
after 443.075 seconds, before pre-memory, with peak RSS 1511724 KiB. It is
|
||||
neither a timeout nor a completed A/B performance baseline.
|
||||
|
||||
For completed macro acceptance, the Crypto hierarchy MUST contain all six
|
||||
physical banks, zero algorithm ROMs, no inferred key/schedule/Montgomery RAM,
|
||||
and at most the 1184 allowed FIFO memory bits at the unchanged pre-memory
|
||||
checkpoint. Inventory packed vectors/FF bits separately to catch hidden
|
||||
copies; do not lower memories early to make a report appear empty. Fully
|
||||
report every remaining register group and the logical/physical capacity
|
||||
overhead. Do not shrink algorithms, FIFO depths or RSA widths to pass a gate.
|
||||
|
||||
Archive per-hierarchy initial/pre-memory/post-memory/pre-tech counts, inferred
|
||||
memory ports and geometry, macro types/instances, mapped FF/mux counts,
|
||||
standard-cell and macro area, pass timings (memory, opt_dff, techmap, ABC and
|
||||
whole flow), peak RSS, tool versions, input hashes and raw logs. Run both a
|
||||
Crypto block comparison and a whole-SoC comparison. Require measured block
|
||||
synthesis time/RSS improvement; do not claim whole-chip timeout resolution
|
||||
unless the unchanged full-chip flow actually completes. Timeouts retain their
|
||||
failed status and partial evidence. Keep `opt_dff -sat -nodffe -nosdff`, the
|
||||
180-minute Yosys limit and global warning/metric policies unchanged.
|
||||
|
||||
Resolve all six macros to the existing IHP130 Liberty views for synthesis/STA
|
||||
and Verilog models for simulation. Register actual instance paths and
|
||||
LEF/GDS/CDL, power hooks and non-overlapping placement in both applicable
|
||||
LibreLane deliveries; mapping-only evidence is not PnR/signoff. Report
|
||||
WNS/TNS, reset/enable fanout and SRAM setup/hold separately. Existing whole-SoC
|
||||
timing debt does not excuse a new uncharacterized macro or missing timing path.
|
||||
|
||||
Expected artifact classes are `constants.json`, `storage-inventory.json`,
|
||||
`functional.json`, `lifecycle.json`, `cycles.json`, `memory-synthesis-ab.json`
|
||||
and `physical.json`, under the selected variant. Each carries source/config/
|
||||
tool identity, exact command, verdict and raw evidence paths. Missing tools,
|
||||
missing full-width tests or interrupted flows are NOT_RUN/failed, never PASS.
|
||||
|
||||
## Storage Refreeze Development Phases
|
||||
|
||||
The historical V1 development list below is not a set of stable phase IDs.
|
||||
The following new phase IDs are frozen; later work must not renumber them.
|
||||
|
||||
### Phase 0 - Baseline and Storage Verification
|
||||
|
||||
ID: `CRYPTO-P0`. Establish the baseline, independent CRYC1 image/oracle,
|
||||
storage/FF inventory and verification harnesses before modifying production
|
||||
datapaths. Preserve V1 public behavior. Map the six-bank contract to existing
|
||||
Common/technology modules, lock APB/HAL parity expectations and prepare
|
||||
full-width numerical/lifecycle/cycle comparisons. Dependencies are the existing
|
||||
Crypto contract, dependency lock and committed IHP130 profile. Completion
|
||||
requires reproducible constant identities and traceable baseline reports;
|
||||
interrupted historical synthesis is not a successful baseline.
|
||||
|
||||
Validation: existing Crypto primitive/stream/APB tests and register parity,
|
||||
independent image verification and baseline synthesis inventory. This phase
|
||||
adds verification/build artifacts only; it does not advertise V2, change an
|
||||
address/IRQ/DMA allocation or enable incomplete engines.
|
||||
|
||||
The P0 tooling entry points are now available; availability is not a PASS
|
||||
verdict. Set one `BUILD_TIMESTAMP` for the entire sequence:
|
||||
|
||||
```sh
|
||||
make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS crypto-p0-constants
|
||||
make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS crypto-p0-rtl
|
||||
make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS crypto-p0-baseline
|
||||
make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS crypto-p0-report
|
||||
```
|
||||
|
||||
Artifacts are under `build/<variant>/crypto/p0/`. `functional.json` identifies
|
||||
the standalone tests, exact commands/logs and source identity; `cycles.json`
|
||||
records V1 measurement conditions. `baseline-synthesis.json` and
|
||||
`storage-inventory.json` distinguish generic memories, explicit register bits
|
||||
and physical macros at initial, pre-memory, pre-SAT and mapped checkpoints.
|
||||
The block driver wraps the existing SoC script without dropping SAT or ABC
|
||||
passes. V1/P0 has zero Crypto macros; the six-bank count is a P1 expectation.
|
||||
`p0-report.json` stays incomplete if any required stage is absent, stale or
|
||||
unsuccessful. Full-SoC A/B and V2 physical acceptance remain P2 work.
|
||||
|
||||
P0 baseline evidence collected on 2026-09-25 is below
|
||||
`build/ihp130-2026-09-25-10-23-9903b3d112b7/crypto/p0/`. The aggregate
|
||||
`p0-report.json` passes its constant, functional, cycle, inventory and block
|
||||
synthesis gates. Its source hashes establish that production Crypto RTL still
|
||||
matches baseline `92830d9`; this is not V2 implementation evidence.
|
||||
|
||||
| P0 observation | Measured result |
|
||||
| --- | --- |
|
||||
| CRYC1 generation | Independent mathematical oracle and six data/padding corruption cases pass. |
|
||||
| Functional suite | Seven fixtures pass: four existing V1 tests, CBC/CTR plus 22 SHA padding cases, actual Crypto-DMA, and full RSA-2048. |
|
||||
| RSA-2048 | Prepare 283077 cycles; public 65537 operation 187352; private and bad-private-exponent rejection both 26382568 cycles. |
|
||||
| AES DMA | Four ECB blocks / 64 bytes, AXI stalls, exact output/address/count checks: 75 engine cycles. |
|
||||
| Initial / pre-memory inferred storage | Both 91712 bits: 83968 ROM and 7744 RAM. |
|
||||
| Explicit generic register bits | Initial 44724; pre-memory 44719; pre-SAT 52591; post-SAT 52587. SRAM macro count is zero. |
|
||||
| Block synthesis | Unchanged balanced recipe, PCLK target 20833 ps, exit 0; 2056.928 seconds and peak RSS 2011792 KiB. |
|
||||
| Major phase wall times | Memory 1.518 s; explicit SAT opt_dff 1499.169 s; techmap 8.852 s; ABC 391.591 s. |
|
||||
| Mapped block | 324973 cells, 5153548.201206 um2 total standard-cell area; 2576174.025598 um2 sequential area. |
|
||||
|
||||
The explicit SAT pass removed four AES register bits; RSA core register bits
|
||||
remained 32938. Auxiliary checkpoint replays completed APB-wrapper, AES-core,
|
||||
other AES/SHA/FIFO and Montgomery selections in approximately 15.9, 12.7, 6.9
|
||||
and 12.2 seconds. RSA selections remained active longer and were intentionally
|
||||
stopped after the main baseline completed. These are localization observations,
|
||||
not substitute synthesis passes or timeout verdicts. The main run used the
|
||||
original complete selection and completed successfully. Auxiliary jobs and
|
||||
brief debugger sampling overlapped part of its runtime; repeat isolated A/B
|
||||
runs for any strict performance-improvement claim.
|
||||
|
||||
ABC warned that its shared SCL cache rename failed and fell back to Liberty;
|
||||
record and match that frontend mode in later comparisons. The combinational
|
||||
network warning is also retained in raw logs. No warning baseline was changed.
|
||||
Ruff, changed-testbench Verible checks and diff checks pass. The full Make
|
||||
format check reports existing differences in Makefile, APU software/block
|
||||
rules, GA2D/Yosys rules and formal.mk; the new Make entries introduce no
|
||||
formatter delta. Pytest was not run under the maintainer's instruction.
|
||||
Firmware/full-SoC simulation, full-chip synthesis, netlist simulation, STA,
|
||||
formal proofs and physical qualification were not run by this P0 delivery.
|
||||
They remain explicit later gates; the P0 aggregate is not an all-gates release
|
||||
verdict or a claim that SRAM has resolved the whole-SoC synthesis problem.
|
||||
|
||||
### Phase 1 - Macro-backed Crypto and Secure Initialization
|
||||
|
||||
ID: `CRYPTO-P1`. Depends on P0. Implement all six banks, constant loading,
|
||||
scrub/readback/lock, AES/SHA microsteps, SRAM-based RSA/Montgomery and the full
|
||||
V2 APB/HAL/LP-startup contract together. Preserve the allowed FIFO capacities,
|
||||
numerical behavior and concurrency. Update filelists/models, manual register
|
||||
mirrors and tests. The public change is V2 initialization/maintenance discovery,
|
||||
bounded APB waits, new HAL APIs and delayed erase completion; system address,
|
||||
DMA/IRQ and clock/reset allocations stay fixed.
|
||||
|
||||
Completion requires the full verification matrix, cycle ceilings, all six
|
||||
macro instances in block synthesis and no prohibited inferred stores/copies.
|
||||
Validate focused simulations/formal first, then RTL style/lint, embedded-C
|
||||
format/policy/host tests, affected firmware and full-SoC behavioral selftest.
|
||||
Do not call the refreeze implemented merely because wrappers elaborate.
|
||||
|
||||
### Phase 2 - Synthesis and Integration Qualification
|
||||
|
||||
ID: `CRYPTO-P2`. Depends on P1. Complete like-for-like block/SoC synthesis,
|
||||
netlist boot plus Crypto functional evidence, STA, physical-view registration,
|
||||
remaining PDK/fallback compatibility and the release report. Keep block
|
||||
functional netlist tests in addition to whole-chip UART boot; boot alone does
|
||||
not exercise Crypto. Refresh guide/status claims only from current evidence.
|
||||
|
||||
Use these existing commands for the applicable gates (focused new harness
|
||||
commands are added and documented by P0/P1 before use):
|
||||
|
||||
```sh
|
||||
make rtl-format-check rtl-style-check-all rtl-readiness-check-all
|
||||
make CONFIG=configs/ci/ihp130.mk SIMU=VERILATOR HAVE_SVA=YES rtl-lint
|
||||
make sw-format-check sw-policy-check sw-host-test
|
||||
make CONFIG=configs/ci/ihp130.mk APP=ci_smoke firmware
|
||||
make CONFIG=configs/ci/ihp130.mk APP=ci_smoke LINK_TYPE=ld2_all_sram \
|
||||
SIMU=VERILATOR HAVE_SVA=YES VERILATOR_SIM_ARGS=--fast-flash \
|
||||
SOC_SIM_TIME=1800 firmware sim
|
||||
python3 scripts/regress.py --root . --suite pr --pdk IHP130 --netsim-boot-only
|
||||
make CONFIG=configs/ci/ihp130.mk librelane-doctor
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Use a single explicit BUILD_TIMESTAMP for commands that must consume the same
|
||||
variant; behavioral and synthesis configuration differences must be recorded.
|
||||
Python tooling changes additionally require Ruff/Pytest under repository
|
||||
policy, subject to the maintainer's active execution constraints. Existing CI
|
||||
behavioral-only jobs are not synthesis/STA evidence. P2 completion requires
|
||||
numerical/lifecycle PASS, macro and residual-register audit, measured block
|
||||
time/RSS improvement, full-flow verdicts and explicit physical/security gaps.
|
||||
|
||||
## Security Boundary and Claims
|
||||
|
||||
The MVP provides useful logical controls: management-only address decoding,
|
||||
no key/exponent readback, busy-time configuration rejection, sticky error
|
||||
reporting, deterministic private-RSA scheduling, post-operation RSA checking,
|
||||
abort, and synchronous zeroization. Reset and zeroize drive all self-owned
|
||||
secret-bearing state to zero; FIFOs are flushed.
|
||||
abort, and zeroization. The V1 intent is clearing secret state and flushing
|
||||
FIFOs; this is not evidence of physical SRAM erasure. V2 requires immediate
|
||||
logical invalidation on reset or accepted zeroize, followed by the bounded
|
||||
physical scrub/readback protocol above. Its completion is qualified by a
|
||||
running PCLK; stopped-clock retention is not a successful clear.
|
||||
|
||||
It is not a secure element and makes no resistance claim against DPA, EM
|
||||
analysis, clock/voltage/laser fault injection, invasive probing, scan access,
|
||||
@@ -242,6 +790,7 @@ only a mechanical subset of MISRA C:2012 Amendment 2 and is not a certification.
|
||||
|
||||
### IHP130 implementation evidence
|
||||
|
||||
The following historical V1 evidence does not qualify the SRAM refreeze.
|
||||
The 2026-08-21 PR regression used the committed IHP130 profile at 72 MHz and
|
||||
completed RTL simulation, synthesis, netlist boot simulation, OpenSTA, warning
|
||||
review, and metric collection. The synthesized SoC contains 793,473 cells with
|
||||
@@ -269,9 +818,11 @@ paths, and maximum-fanout intent reviewed by the integration owner.
|
||||
|
||||
## Performance and Security Optimization Roadmap
|
||||
|
||||
The current design is a balanced, low-integration-risk baseline. Optimize only
|
||||
from measured IHP130 synthesis/STA/power data and preserve a small reference
|
||||
configuration for equivalence and regression.
|
||||
The approved next delivery is the compact SRAM implementation in CRYPTO-P0
|
||||
through CRYPTO-P2. The following commercial variants are later work, not
|
||||
permission to restore ROMs or inferred operand arrays in that delivery.
|
||||
Optimize from measured IHP130 synthesis/STA/power data and retain a
|
||||
verification-only V1 reference for numerical comparisons.
|
||||
|
||||
1. Harden the current interface. Add formal APB assertions, stable result
|
||||
properties, zeroize/abort reachability, FIFO conservation, and independent
|
||||
@@ -288,10 +839,10 @@ configuration for equivalence and regression.
|
||||
tag failure in a protected API.
|
||||
4. Scale SHA. Add SHA-384/512 with a 64-bit datapath variant, HMAC with inner/
|
||||
outer state isolation, context save/restore, and two context banks. First
|
||||
replace the indexed SHA-256 schedule ring with a shift schedule so the round
|
||||
counter does not decode onto the schedule write path. A two-round-per-cycle
|
||||
option is reasonable only after the one-round implementation closes the
|
||||
target timing with margin.
|
||||
qualify a separately banked/pipelined schedule organization for a throughput
|
||||
variant. The compact refreeze uses the synchronous SRAM schedule above;
|
||||
restoring a shift-register schedule needs a separate variant specification.
|
||||
A two-round-per-cycle option needs its own timing and numerical evidence.
|
||||
5. Improve RSA performance. Parameterize the Montgomery limb multiplier for
|
||||
one, two, or four 32x32 MAC lanes; add a registered multiply stage,
|
||||
carry-save accumulation, banked operand SRAM, and locally registered control
|
||||
|
||||
@@ -46,6 +46,15 @@ The default ABC target period is derived from the `external` STA period in
|
||||
`YOSYS_TARGET_PERIOD_PS=<ps>` when needed.
|
||||
|
||||
Synthesis outputs are generated below `build/` and must not be committed.
|
||||
The CRYPTO-P0 baseline uses `make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS
|
||||
crypto-p0-baseline`. Its wrapper in `yosys/script/crypto_block_synth.tcl`
|
||||
sources the ordinary SoC script and balanced ABC recipe without optimization
|
||||
changes, while exporting JSON checkpoints for inferred RAM/ROM and register
|
||||
accounting. The standalone Crypto target uses the inventory's PCLK period.
|
||||
Its reports live in `build/<variant>/crypto/p0/`; the macro-backed V2 candidate
|
||||
and whole-chip A/B qualification are later phases in
|
||||
[`docs/ip/crypto.md`](../../../docs/ip/crypto.md).
|
||||
|
||||
Review warning and metric changes with `make check-warnings check-metrics` for
|
||||
the affected profile. `make metrics` writes the recipe-specific file listed
|
||||
above; `make check-metrics` checks that same file.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
# SPDX-License-Identifier: MulanPSL-2.0
|
||||
# Diagnostic checkpoints around the UNCHANGED full-SoC synthesis recipe.
|
||||
# The only elaboration difference is the explicit standalone interface boundary.
|
||||
rename yosys crypto_original_yosys
|
||||
proc yosys {args} {
|
||||
set command [lindex $args 0]
|
||||
if {$command eq "read_slang"} {
|
||||
lappend args --allow-toplevel-iface-ports
|
||||
}
|
||||
if {$command eq "memory"} {
|
||||
crypto_original_yosys write_json "$::env(REPORTS)/pre_memory_design.json"
|
||||
}
|
||||
if {$command eq "opt_dff" && [lsearch -exact $args -sat] >= 0} {
|
||||
crypto_original_yosys write_json "$::env(REPORTS)/pre_sat_design.json"
|
||||
}
|
||||
set result [uplevel 1 [list crypto_original_yosys {*}$args]]
|
||||
if {$command eq "proc"} {
|
||||
crypto_original_yosys write_json "$::env(REPORTS)/initial_design.json"
|
||||
}
|
||||
if {$command eq "opt_dff" && [lsearch -exact $args -sat] >= 0} {
|
||||
crypto_original_yosys write_json "$::env(REPORTS)/post_sat_design.json"
|
||||
}
|
||||
return $result
|
||||
}
|
||||
source [file join [file dirname [info script]] synth.tcl]
|
||||
crypto_original_yosys write_json "$::env(REPORTS)/mapped_design.json"
|
||||
@@ -120,6 +120,15 @@ RSA-2048 Montgomery exponentiation. Its register ABI, key/zeroize boundary,
|
||||
commercial survey, and verification roadmap are documented in
|
||||
[AES/SHA-2/RSA Crypto Controller](../docs/ip/crypto.md).
|
||||
|
||||
The Crypto storage refreeze requires six private `tc_sram_1024x32` banks for
|
||||
constants, keys, SHA schedule and RSA limb storage, with LP initialization,
|
||||
APB V2 discovery and bounded physical scrub/readback. It replaces the old
|
||||
one-round-per-cycle AES/SHA contract with bounded microsteps while preserving
|
||||
algorithms and engine concurrency. Three small stream FIFOs remain explicit
|
||||
inferred-memory exceptions. CRYPTO-P0/P1/P2 define the baseline, implementation
|
||||
and qualification work; the current RTL must not be described as already
|
||||
macro-backed on the strength of this specification.
|
||||
|
||||
The self-owned JPEG controller provides 8-bit Baseline Sequential encode and
|
||||
decode, five raster formats, a 64-bit AXI4 2D DMA, direct and 128-byte SG-ring
|
||||
jobs, four encoder table contexts, interrupts, and LP/HP transferable resource
|
||||
|
||||
@@ -163,6 +163,27 @@ without modifying them. Run `make rtl-migrate-connections`, then
|
||||
ports and interface fields are intentionally unchanged. Run
|
||||
`make rtl-migrate-names`, then format and lint the resulting diff.
|
||||
|
||||
`crypto_constants.py` packs the frozen CRYC1 image from the V1 tables;
|
||||
`crypto_p0.py` independently checks it against the mathematical oracle in
|
||||
`tests/crypto_reference.py`. The P0 runner also executes existing V1 tests,
|
||||
full-width RSA-2048 public/private/recheck vectors, SHA padding boundaries and
|
||||
a real DMA-to-AES-to-memory test. Missing tools or missing success markers are
|
||||
errors. It invokes the shared simulation verdict checker; an Icarus `$fatal`
|
||||
that exits with code zero is still a failed test.
|
||||
|
||||
The `crypto-p0-constants`, `crypto-p0-rtl`, `crypto-p0-baseline` and
|
||||
`crypto-p0-report` Make targets write below `build/<variant>/crypto/p0/`.
|
||||
Select `CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS` and one BUILD_TIMESTAMP for
|
||||
all four. P0 preserves production RTL/HAL and records V1 lifecycle limitations;
|
||||
it does not implement the six-bank V2 refreeze. Block synthesis calls the
|
||||
unchanged `synth.tcl` and `abc_balanced.script`, adding JSON checkpoints and
|
||||
standalone interface elaboration only. Each new synthesis attempt uses a fresh
|
||||
`synth-*` artifact directory recorded in the report, preventing an old netlist
|
||||
from masking a new failed attempt. Its 20833 ps target is derived from
|
||||
the PCLK inventory; it is not a full-chip timing result. A timeout/interruption
|
||||
retains partial checkpoints and cannot pass the aggregate report. See
|
||||
[`../docs/ip/crypto.md`](../docs/ip/crypto.md).
|
||||
|
||||
Update or add tests in [`../tests`](../tests) for script behavior. Run
|
||||
`ruff check .` and `python3 -m pytest -q`; build-flow changes also require the
|
||||
relevant dry-run or regression profile from [`AGENTS.md`](../AGENTS.md).
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pack the frozen CRYC1 image from the revision-qualified V1 RTL tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
|
||||
LAYOUT_ID = 0x43525901
|
||||
WORDS = 2048
|
||||
CRC32 = 0x99CA52FE
|
||||
SHA256 = "b662a0729bf85789aa9c9bd4a9f8a13ebe44e296deebda486bef2792b1e9d47f"
|
||||
|
||||
|
||||
def lookup(source: str, name: str, size: int, base: int) -> list[int]:
|
||||
match = re.search(r"function automatic[^\n]*\b" + name + r"\(.*?endfunction", source, re.S)
|
||||
if match is None:
|
||||
raise ValueError(f"missing {name}")
|
||||
body = match.group()
|
||||
entries: dict[int, int] = {}
|
||||
for key, value in re.findall(
|
||||
r"(?:8'h|6'd)([0-9a-f]+):\s*" + name + r"\s*=\s*(?:8|32)'h([0-9a-f]+)", body
|
||||
):
|
||||
index = int(key, base)
|
||||
if index in entries:
|
||||
raise ValueError(f"duplicate {name}[{index}]")
|
||||
entries[index] = int(value, 16)
|
||||
default = re.search(r"default:\s*" + name + r"\s*=\s*(?:8|32)'h([0-9a-f]+)", body)
|
||||
if default is None or set(entries) != set(range(size - 1)):
|
||||
raise ValueError(f"incomplete {name}")
|
||||
return [entries[i] for i in range(size - 1)] + [int(default[1], 16)]
|
||||
|
||||
|
||||
def pack(root: Path) -> bytes:
|
||||
package = (root / "rtl/ip/security/crypto_pkg.sv").read_text()
|
||||
engine = (root / "rtl/ip/security/crypto_sha2_engine.sv").read_text()
|
||||
words = [0] * WORDS
|
||||
words[:256] = lookup(package, "aes_sbox", 256, 16)
|
||||
words[256:512] = lookup(package, "aes_inverse_sbox", 256, 16)
|
||||
# Rcon is algorithmic in V1; preserve its defined zero at indices 0 and 15.
|
||||
rcon = 1
|
||||
for index in range(1, 15):
|
||||
words[512 + index] = rcon
|
||||
rcon = ((rcon << 1) ^ (0x11B if rcon & 0x80 else 0)) & 0xFF
|
||||
words[1024:1088] = lookup(package, "sha2_k", 64, 10)
|
||||
initial = re.findall(r"s_hash_state_q <= 256'h([0-9a-f]{64});", engine)
|
||||
if len(initial) != 2:
|
||||
raise ValueError("expected SHA-256 then SHA-224 initialization")
|
||||
for start, value in ((1088, initial[1]), (1096, initial[0])):
|
||||
words[start:start + 8] = [int(value[i:i + 8], 16) for i in range(0, 64, 8)]
|
||||
return struct.pack("<2048I", *words)
|
||||
|
||||
|
||||
def validate(payload: bytes) -> dict[str, object]:
|
||||
digest = hashlib.sha256(payload).hexdigest()
|
||||
crc = zlib.crc32(payload)
|
||||
if len(payload) != 8192 or digest != SHA256 or crc != CRC32:
|
||||
raise ValueError("CRYC1 payload does not match the frozen identity")
|
||||
return {"layout_id": f"0x{LAYOUT_ID:08x}", "bytes": len(payload),
|
||||
"words": WORDS, "crc32": f"0x{crc:08x}", "sha256": digest}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
payload = pack(args.root)
|
||||
manifest = validate(payload)
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
(args.output / "crypto.cryc").write_bytes(payload)
|
||||
(args.output / "crypto_constants.inc").write_text(
|
||||
"/* Generated CRYC1 words; do not edit. */\n" +
|
||||
"\n".join(f"UINT32_C(0x{word:08x})," for (word,) in struct.iter_unpack("<I", payload)) + "\n"
|
||||
)
|
||||
(args.output / "constants.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CRYPTO-P0 evidence entry points; never modify production RTL or HAL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts.crypto_constants import pack, validate # noqa: E402
|
||||
|
||||
|
||||
BASELINE = "92830d963da2f1e39bb219c0d377bc4889c07755"
|
||||
COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||
CRYPTO = ROOT / "rtl/ip/security"
|
||||
RTL = [ROOT / "rtl" / line.lstrip("/") for line in
|
||||
(ROOT / "rtl/mini/filelist/ip.fl").read_text().splitlines()
|
||||
if line.startswith("/ip/security/crypto_") or line == "/ip/security/apb4_crypto.sv"]
|
||||
INTERFACES = [COMMON / "interface/apb4_if.sv", COMMON / "interface/axi4_stream_if.sv"]
|
||||
UTILITIES = [COMMON / "utils/register.sv", COMMON / "utils/fifo.sv"]
|
||||
V2_OFFSETS = {"MEM_STATUS": 0x28, "MEM_CONTROL": 0x2C, "TABLE_ID": 0x30,
|
||||
"TABLE_WORDS": 0x34, "TABLE_DATA": 0x38, "TABLE_CRC": 0x3C,
|
||||
"MEM_ERROR": 0x40, "MEM_CYCLES": 0x44}
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def dump(path: Path, data: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def oracle():
|
||||
spec = importlib.util.spec_from_file_location("crypto_p0_reference", ROOT / "tests/crypto_reference.py")
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError("independent oracle unavailable")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def identity(variant: Path) -> dict:
|
||||
manifest = json.loads((variant / "meta/manifest.json").read_text())
|
||||
if manifest["configuration"]["PDK"] != "IHP130" or manifest["profile"] != "ihp130":
|
||||
raise ValueError("CRYPTO-P0 baseline requires configs/ci/ihp130.mk")
|
||||
sources = [*RTL, *INTERFACES, *UTILITIES, ROOT / "rtl/tech/tc_sram.sv",
|
||||
ROOT / "crt/include/retrosoc/hal/crypto_regs.h", ROOT / "crt/src/hal/crypto.c"]
|
||||
production = {str(p.relative_to(ROOT)): digest(p) for p in sources}
|
||||
matching = all(hashlib.sha256(subprocess.check_output(
|
||||
["git", "show", f"{BASELINE}:{p.relative_to(ROOT)}"], cwd=ROOT
|
||||
)).hexdigest() == digest(p) for p in RTL)
|
||||
return {"schema_version": 1, "baseline_revision": BASELINE,
|
||||
"production_rtl_matches_baseline": matching, "production_sha256": production,
|
||||
"manifest": manifest, "driver_sha256": digest(Path(__file__)),
|
||||
"constants_generator_sha256": digest(ROOT / "scripts/crypto_constants.py"),
|
||||
"oracle_sha256": digest(ROOT / "tests/crypto_reference.py")}
|
||||
|
||||
|
||||
def execute(directory: Path, name: str, command: list[str], *, timeout: int = 1800,
|
||||
env: dict[str, str] | None = None, marker: str | None = None) -> dict:
|
||||
missing = shutil.which(command[0])
|
||||
if missing is None:
|
||||
raise FileNotFoundError(f"required tool missing: {command[0]}")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
log = directory / f"{name}.log"
|
||||
result = directory / f"{name}.json"
|
||||
flow = [sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", name,
|
||||
"--log", str(log), "--result", str(result), "--cwd", str(ROOT)]
|
||||
for key, value in (env or {}).items():
|
||||
flow += ["--env", f"{key}={value}"]
|
||||
flow += ["--", "timeout", "--foreground", "--kill-after=5s", f"{timeout}s", *command]
|
||||
run = subprocess.run(flow, cwd=ROOT, check=False)
|
||||
evidence = json.loads(result.read_text())
|
||||
if run.returncode or evidence["status"] != "passed":
|
||||
raise RuntimeError(f"{name}: see {result}")
|
||||
if marker:
|
||||
checked = subprocess.run([sys.executable, str(ROOT / "scripts/check_simulation.py"),
|
||||
"--log", str(log), "--result", str(directory / f"{name}-check.json"),
|
||||
"--require", marker], check=False)
|
||||
if checked.returncode:
|
||||
raise RuntimeError(f"{name}: simulation verdict rejected {log}")
|
||||
return {"result": str(result), "log": str(log), "marker": marker,
|
||||
"duration_seconds": evidence["duration_seconds"],
|
||||
"peak_rss_kib": evidence["peak_rss_kib"]}
|
||||
|
||||
|
||||
def constants(variant: Path, output: Path) -> None:
|
||||
payload = pack(ROOT)
|
||||
mathematical = oracle().constant_image()
|
||||
if payload != mathematical or payload != pack(ROOT):
|
||||
raise ValueError("independent image/packing/reproducibility mismatch")
|
||||
data = validate(payload)
|
||||
# Exercise rejection of corruption in both used and padding regions.
|
||||
for index in (0, 1023, 2048, 4095, 4096, 8191):
|
||||
bad = bytearray(payload)
|
||||
bad[index] ^= 1
|
||||
try:
|
||||
validate(bytes(bad))
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError(f"corruption accepted at byte {index}")
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
(output / "crypto.cryc").write_bytes(payload)
|
||||
words = [int.from_bytes(payload[i:i + 4], "little") for i in range(0, 8192, 4)]
|
||||
(output / "crypto_constants.inc").write_text(
|
||||
"/* Generated public CRYC1 image. */\n" +
|
||||
"\n".join(f"UINT32_C(0x{value:08x})," for value in words) + "\n")
|
||||
dump(output / "constants.json", {**identity(variant), **data, "status": "passed",
|
||||
"independent_oracle": "GF(2^8) AES, exact integer prime-root SHA",
|
||||
"corruption_cases": 6, "expected_v2_offsets": V2_OFFSETS,
|
||||
"v2_implementation": "NOT_RUN (P1)", "planned_physical_banks": 6,
|
||||
"planned_physical_bytes": 24576, "allowed_inferred_fifo_bits": 1184,
|
||||
"v2_contract": {"ip_version": "0x00020000", "layout_id": "0x43525901",
|
||||
"mem_status_reset": 4, "status_reset": 8, "memory_irq_bit": 5,
|
||||
"memory_error_bit": 4, "scrub_max_cycles": 2056,
|
||||
"verify_max_cycles": 8192, "apb_max_wait_cycles": 4}})
|
||||
|
||||
|
||||
def parity() -> dict:
|
||||
# Read-only V1 mirror validation, separate from Pytest execution.
|
||||
sv = (CRYPTO / "crypto_define.svh").read_text()
|
||||
header = (ROOT / "crt/include/retrosoc/hal/crypto_regs.h").read_text()
|
||||
offsets = dict(re.findall(r"`define\s+APB4_CRYPTO__(\w+)\s+12'h([0-9a-fA-F]+)", sv))
|
||||
checked = 0
|
||||
for name, value in offsets.items():
|
||||
match = re.search(r"#define\s+RS_CRYPTO_REG_" + name + r"\s+UINT32_C\((0x[0-9a-fA-F]+)\)", header)
|
||||
if match is None or int(match[1], 16) != int(value, 16):
|
||||
raise ValueError(f"V1 offset parity: {name}")
|
||||
checked += 1
|
||||
if checked < 40:
|
||||
raise ValueError("V1 offset inventory incomplete")
|
||||
# Read the existing handwritten mapping as data; do not invoke Pytest/tests.
|
||||
tree = ast.parse((ROOT / "tests/test_crypto_register_parity.py").read_text())
|
||||
function = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and
|
||||
n.name == "test_crypto_register_fields_match_rtl")
|
||||
literals = {n.targets[0].id: ast.literal_eval(n.value) for n in function.body
|
||||
if isinstance(n, ast.Assign) and isinstance(n.targets[0], ast.Name)
|
||||
and n.targets[0].id in ("mapping", "shift_names")}
|
||||
for rtl_name, c_name in literals["mapping"].items():
|
||||
bit = re.search(r"`define\s+" + rtl_name + r"\s+(\d+)\s*$", sv, re.M)
|
||||
value = re.search(r"#define\s+" + c_name +
|
||||
r"\s+(?:UINT32_C\((0x[0-9a-fA-F]+)\)|(\d+)U)", header)
|
||||
if bit is None or value is None:
|
||||
raise ValueError(f"missing parity field {rtl_name}/{c_name}")
|
||||
expected = int(bit[1]) if c_name in literals["shift_names"] else 1 << int(bit[1])
|
||||
actual = int(value[1], 16) if value[1] else int(value[2])
|
||||
if actual != expected:
|
||||
raise ValueError(f"V1 field parity: {rtl_name}/{c_name}")
|
||||
return {"offsets_checked": checked, "fields_checked": len(literals["mapping"]),
|
||||
"v2_offsets_reserved_in_spec": V2_OFFSETS}
|
||||
|
||||
|
||||
def rtl(variant: Path, output: Path, jobs: int, selected: str | None = None) -> None:
|
||||
report = {**identity(variant), "status": "running", "cases": [], "parity": parity()}
|
||||
report_path = output / (f"functional-{selected}.json" if selected else "functional.json")
|
||||
dump(report_path, report)
|
||||
cases = [("crypto_primitive_tb", "Crypto AES and SHA primitive tests passed"),
|
||||
("crypto_engine_tb", "Crypto streaming engine tests passed"),
|
||||
("crypto_apb_tb", "Crypto APB register tests passed"),
|
||||
("crypto_rsa_tb", "Crypto RSA Montgomery tests passed"),
|
||||
("crypto_vectors_p0_tb", "CRYPTO_P0_VECTORS_PASS"),
|
||||
("crypto_dma_p0_tb", "CRYPTO_P0_DMA_PASS"),
|
||||
("crypto_rsa2048_tb", "CRYPTO_P0_RSA2048_PASS")]
|
||||
try:
|
||||
sha_vector = output / "sha-padding.hex"
|
||||
sha_vector.write_text("\n".join(
|
||||
((hashlib.sha224(bytes(range(n))).hexdigest() + "00000000") if mode == 0 else
|
||||
hashlib.sha256(bytes(range(n))).hexdigest()) for mode in (0, 1)
|
||||
for n in (0, 1, 3, 55, 56, 63, 64, 65, 127, 128, 129)) + "\n")
|
||||
fixture = oracle().rsa_fixture()
|
||||
dump(output / "rsa2048-public-fixture.json", {k: hex(v) for k, v in fixture.items()})
|
||||
vector = output / "rsa2048.hex"
|
||||
vector.write_text("\n".join(f"{fixture[k]:0512x}" for k in
|
||||
("modulus", "private", "message", "ciphertext", "n0_prime", "montgomery")) + "\n")
|
||||
for top, marker in cases:
|
||||
if selected is not None and top != selected:
|
||||
continue
|
||||
case = output / "rtl" / top
|
||||
case.mkdir(parents=True, exist_ok=True)
|
||||
sources = [*INTERFACES, *UTILITIES, *[p for p in RTL if p.suffix == ".sv"]]
|
||||
includes = [CRYPTO, COMMON, COMMON / "interface"]
|
||||
if top == "crypto_dma_p0_tb":
|
||||
generated = case / "memory_map"
|
||||
execute(case, "memory-map", [sys.executable, str(ROOT / "rtl/mini/address_map/generate_memory_map.py"),
|
||||
"--map", str(ROOT / "rtl/mini/address_map/memory_map.json"),
|
||||
"--output-dir", str(generated), "--have-sram-if", "NO"])
|
||||
includes += [generated / "rtl", COMMON / "stream", COMMON / "utils"]
|
||||
sources = [COMMON / "interface/axi4_if.sv", *sources,
|
||||
COMMON / "stream/round_robin_arbiter.sv"]
|
||||
sources += [ROOT / "rtl/ip/peripheral" / n for n in
|
||||
("dma_pkg.sv", "dma_req_if.sv", "dma_axi4_master.sv", "dma_core.sv")]
|
||||
sources.append(ROOT / "tests/rtl" / f"{top}.sv")
|
||||
if top == "crypto_rsa2048_tb":
|
||||
(case / "ccache-tmp").mkdir(exist_ok=True)
|
||||
execute(case, "compile", ["verilator", "--binary", "--timing", "-Wno-fatal",
|
||||
"--top-module", top, "--Mdir", str(case / "obj"), "-j", str(jobs),
|
||||
*[f"-I{p}" for p in includes], *map(str, sources)],
|
||||
env={"CCACHE_DIR": str(case / "ccache"), "CCACHE_TEMPDIR": str(case / "ccache-tmp")})
|
||||
command = [str(case / "obj" / f"V{top}"), f"+vectors={vector}"]
|
||||
else:
|
||||
fl = case / "sources.fl"
|
||||
fl.write_text("+define+SV_ASSRT_DISABLE\n" +
|
||||
"\n".join([*[f"+incdir+{p}" for p in includes], *map(str, sources)]) + "\n")
|
||||
converted = case / "test.v"
|
||||
execute(case, "convert", [sys.executable, str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||
"-f", str(fl), "--output", str(converted)])
|
||||
binary = case / "test.vvp"
|
||||
execute(case, "compile", ["iverilog", "-g2012", "-s", top,
|
||||
"-o", str(binary), str(converted)])
|
||||
command = ["vvp", str(binary)]
|
||||
if top == "crypto_vectors_p0_tb":
|
||||
command.append(f"+sha={sha_vector}")
|
||||
evidence = execute(case, "simulation", command, marker=marker)
|
||||
report["cases"].append({"top": top, "status": "passed",
|
||||
"testbench_sha256": digest(ROOT / "tests/rtl" / f"{top}.sv"), **evidence})
|
||||
dump(report_path, report)
|
||||
report["status"] = "passed"
|
||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||
report.update(status="failed", error=str(error))
|
||||
raise
|
||||
finally:
|
||||
dump(report_path, report)
|
||||
cycles = []
|
||||
for case in report["cases"]:
|
||||
cycles += [{"operation": op, "cycles": int(count), "log": case["log"]} for op, count in
|
||||
re.findall(r"CRYPTO_CYCLES operation=(\w+) cycles=(\d+)", Path(case["log"]).read_text())]
|
||||
sha_cycles = []
|
||||
for case in report["cases"]:
|
||||
sha_cycles += [{"sha256": bool(int(mode)), "bytes": int(size), "cycles": int(count)}
|
||||
for mode, size, count in re.findall(r"CRYPTO_SHA_CASE mode=(\d) bytes=(\d+) cycles=(\d+)",
|
||||
Path(case["log"]).read_text())]
|
||||
dump(output / (f"cycles-{selected}.json" if selected else "cycles.json"),
|
||||
{"status": report["status"], "measurements": cycles, "sha_padding": sha_cycles,
|
||||
"scope": "V1 baseline, not V2 latency qualification"})
|
||||
|
||||
|
||||
def number(value: int | str) -> int:
|
||||
return value if isinstance(value, int) else int(value, 2)
|
||||
|
||||
|
||||
def synthesis_metrics(report_dir: Path) -> dict:
|
||||
work = report_dir.parent
|
||||
area_path = report_dir / "apb4_crypto_area.json"
|
||||
# Yosys -t also prefixes tee/stat JSON. Keep the original and normalize
|
||||
# only for parsing; write_json checkpoints themselves have no log prefix.
|
||||
area = json.loads(re.sub(r"^\[\d+\.\d+\] ", "", area_path.read_text(), flags=re.M))
|
||||
perf_path = work / "yosys-perf.json"
|
||||
perf = json.loads(perf_path.read_text())
|
||||
log = (work / "yosys.log").read_text()
|
||||
headers = [(float(t), name) for t, name in re.findall(
|
||||
r"^\[(\d+\.\d+)\] \d+\. Executing (\w+)", log, re.M)]
|
||||
timings = {}
|
||||
for (start, name), (end, _) in zip(headers, headers[1:]):
|
||||
if name in ("MEMORY", "OPT_DFF", "TECHMAP", "ABC"):
|
||||
timings[name.lower()] = round(end - start, 6)
|
||||
design = area["design"]
|
||||
return {"mapped_cells": design["num_cells"], "area_um2": design["area"],
|
||||
"sequential_area_um2": design["sequential_area"],
|
||||
"cell_types": design["num_cells_by_type"], "phase_wall_seconds": timings,
|
||||
"yosys_exclusive_pass_seconds": {k: v["runtime_ns"] / 1e9 for k, v in perf["passes"].items()},
|
||||
"timing_note": "MEMORY command exclusive time excludes its child passes; use phase_wall_seconds",
|
||||
"raw_area": str(area_path), "raw_perf": str(perf_path),
|
||||
"yosys_result": json.loads((work / "yosys.json").read_text()),
|
||||
"warnings": [line for line in log.splitlines() if "Warning:" in line]}
|
||||
|
||||
|
||||
def inventory(path: Path) -> dict:
|
||||
design = json.loads(path.read_text())
|
||||
modules = design["modules"]
|
||||
rows = []
|
||||
macros: dict[str, int] = {}
|
||||
|
||||
def visit(name: str, instance: str, ancestors: tuple[str, ...]) -> None:
|
||||
if name in ancestors:
|
||||
raise ValueError("recursive synthesis hierarchy")
|
||||
module = modules[name]
|
||||
cells = module.get("cells", {})
|
||||
writes = {c["parameters"]["MEMID"].lstrip("\\") for c in cells.values()
|
||||
if c["type"].startswith("$memwr")}
|
||||
memories = []
|
||||
for mem_name, mem in module.get("memories", {}).items():
|
||||
ports = [c for c in cells.values() if c["type"].startswith("$mem") and
|
||||
c.get("parameters", {}).get("MEMID", "").lstrip("\\") == mem_name.lstrip("\\")]
|
||||
memories.append({"name": mem_name, "width": mem["width"], "depth": mem["size"],
|
||||
"bits": mem["width"] * mem["size"],
|
||||
"read_ports": sum(c["type"].startswith("$memrd") for c in ports),
|
||||
"write_ports": sum(c["type"].startswith("$memwr") for c in ports),
|
||||
"source": mem.get("attributes", {}).get("src"),
|
||||
"kind": "ram" if mem_name.lstrip("\\") in writes else "rom"})
|
||||
for cell_name, cell in cells.items():
|
||||
if cell["type"] in ("$mem", "$mem_v2"):
|
||||
p = cell["parameters"]
|
||||
memories.append({"name": cell_name, "width": number(p["WIDTH"]),
|
||||
"depth": number(p["SIZE"]), "bits": number(p["WIDTH"]) * number(p["SIZE"]),
|
||||
"read_ports": number(p["RD_PORTS"]), "write_ports": number(p["WR_PORTS"]),
|
||||
"kind": "ram" if number(p["WR_PORTS"]) else "rom"})
|
||||
register_bits = sum(number(c["parameters"]["WIDTH"]) for c in cells.values()
|
||||
if re.match(r"\$(?:a|s|al)?dff", c["type"]) and "WIDTH" in c["parameters"])
|
||||
rows.append({"instance": instance, "module": name, "memories": memories,
|
||||
"inferred_bits": sum(m["bits"] for m in memories),
|
||||
"register_bits": register_bits, "local_cells": len(cells),
|
||||
"registers": [{"cell": n, "type": c["type"], "bits": number(c["parameters"]["WIDTH"]),
|
||||
"source": c.get("attributes", {}).get("src")}
|
||||
for n, c in cells.items() if re.match(r"\$(?:a|s|al)?dff", c["type"])
|
||||
and "WIDTH" in c["parameters"]],
|
||||
"mux_cells": sum("mux" in c["type"].lower() for c in cells.values())})
|
||||
for child, cell in cells.items():
|
||||
cell_type = cell["type"]
|
||||
if cell_type.startswith("RM_IHPSG13_"):
|
||||
macros[cell_type] = macros.get(cell_type, 0) + 1
|
||||
elif cell_type in modules and not number(modules[cell_type].get("attributes", {}).get("blackbox", 0)):
|
||||
visit(cell_type, instance + "." + child, (*ancestors, name))
|
||||
visit("apb4_crypto", "apb4_crypto", ())
|
||||
return {"path": str(path), "sha256": digest(path), "rows": rows, "macros": macros,
|
||||
"inferred_bits": sum(r["inferred_bits"] for r in rows),
|
||||
"register_bits": sum(r["register_bits"] for r in rows),
|
||||
"ram_bits": sum(m["bits"] for row in rows for m in row["memories"] if m["kind"] == "ram"),
|
||||
"rom_bits": sum(m["bits"] for row in rows for m in row["memories"] if m["kind"] == "rom"),
|
||||
"register_bits_scope": "generic RTLIL FFs before library mapping; not mapped standard cells"}
|
||||
|
||||
|
||||
def synth(variant: Path, output: Path) -> None:
|
||||
ident = identity(variant)
|
||||
cfg = ident["manifest"]["configuration"]
|
||||
if not ident["production_rtl_matches_baseline"]:
|
||||
raise ValueError("P0 baseline RTL differs from the frozen baseline revision")
|
||||
if (cfg["HAVE_SRAM_MACRO"] != "YES" or cfg["PDK_BEHAV"] != "NO" or
|
||||
cfg["SYNTH"] != "YOSYS" or cfg["SYNTH_RECIPE"] != "balanced"):
|
||||
raise ValueError("baseline requires macros YES, PDK_BEHAV NO, SYNTH YOSYS, balanced recipe")
|
||||
# Every attempt gets fresh outputs: a new timeout must never inherit an
|
||||
# older successful netlist/checkpoint. The report records the exact paths.
|
||||
work = Path(tempfile.mkdtemp(prefix="synth-", dir=output))
|
||||
for name in ("out", "tmp", "rpt"):
|
||||
(work / name).mkdir(parents=True, exist_ok=True)
|
||||
fl = work / "crypto.fl"
|
||||
fl.write_text("+define+PDK_IHP130\n+define+HAVE_SRAM_MACRO\n+define+SYNTHESIS\n+define+SV_ASSRT_DISABLE\n" +
|
||||
f"+incdir+{CRYPTO}\n+incdir+{COMMON}\n" + f"+incdir+{COMMON / 'interface'}\n" +
|
||||
"\n".join(map(str, [*INTERFACES, *UTILITIES, ROOT / "rtl/tech/tc_sram.sv",
|
||||
*[p for p in RTL if p.suffix == ".sv"]])) + "\n")
|
||||
directory = ROOT / "physical/smoke/syn/yosys/script"
|
||||
period = subprocess.check_output([sys.executable, str(ROOT / "scripts/yosys_period.py"),
|
||||
"--domains", str(ROOT / "rtl/mini/integration/clock_reset_domains.json"), "--domain", "pclk"], text=True).strip()
|
||||
env = {"PDK": "IHP130", "SOC": "MINI", "SYNTH_RECIPE": "balanced", "HAVE_SRAM_MACRO": "YES",
|
||||
"SRAM_SIZE_KIB": "32", "SV_FLIST": str(fl), "TOP_DESIGN": "apb4_crypto",
|
||||
"PROJ_NAME": "apb4_crypto", "WORK": str(work / "tmp"), "BUILD": str(work / "out"),
|
||||
"REPORTS": str(work / "rpt"), "NETLIST": str(work / "out/crypto.v"),
|
||||
"CONFIG": str(work / "out/crypto.config"), "YOSYS_TARGET_PERIOD_PS": period,
|
||||
"YOSYS_FLATTEN_HIER": "1", "YOSYS_KEEP_HIER_INST": '"t:tc_sram*$*"',
|
||||
"YOSYS_REPORT_INSTS": ""}
|
||||
report = {**ident, "status": "running", "scope": "crypto_block", "environment": env,
|
||||
"recipe_sha256": {p.name: digest(p) for p in
|
||||
(directory / "synth.tcl", directory / "abc_balanced.script", directory / "crypto_block_synth.tcl")}}
|
||||
dump(output / "baseline-synthesis.json", report)
|
||||
try:
|
||||
report["flow"] = execute(work, "yosys", ["yosys", "-t", "--perffile", str(work / "yosys-perf.json"),
|
||||
"-c", str(directory / "crypto_block_synth.tcl")], timeout=10800, env=env)
|
||||
report["status"] = "passed"
|
||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||
report.update(status="failed", error=str(error))
|
||||
raise
|
||||
finally:
|
||||
dump(output / "baseline-synthesis.json", report)
|
||||
inventories = {p.stem: inventory(p) for p in (work / "rpt").glob("*_design.json")}
|
||||
dump(output / "storage-inventory.json", {**ident, "status": report["status"],
|
||||
"checkpoints": inventories, "candidate": "NOT_RUN (P1)", "whole_soc": "NOT_RUN"})
|
||||
|
||||
|
||||
def report_all(variant: Path, output: Path) -> None:
|
||||
current = identity(variant)
|
||||
stages = {}
|
||||
for name in ("constants", "functional", "cycles", "baseline-synthesis", "storage-inventory"):
|
||||
path = output / f"{name}.json"
|
||||
stages[name] = json.loads(path.read_text()) if path.exists() else {"status": "NOT_RUN"}
|
||||
for name in ("constants", "functional", "baseline-synthesis", "storage-inventory"):
|
||||
if stages[name].get("status") == "passed" and (
|
||||
stages[name].get("production_sha256") != current["production_sha256"] or
|
||||
stages[name].get("manifest", {}).get("configuration") != current["manifest"]["configuration"]
|
||||
):
|
||||
stages[name] = {"status": "stale", "reason": "source/config identity mismatch"}
|
||||
if stages["functional"].get("status") == "passed":
|
||||
cases = stages["functional"].get("cases", [])
|
||||
if len(cases) != 7:
|
||||
stages["functional"]["status"] = "incomplete"
|
||||
for case in cases:
|
||||
source = ROOT / "tests/rtl" / f"{case['top']}.sv"
|
||||
if case.get("testbench_sha256") != digest(source):
|
||||
stages["functional"]["status"] = "stale"
|
||||
# Refresh inventory from this attempt's checkpoints, without rerunning
|
||||
# synthesis. Partial data is useful but can never pass the aggregate gate.
|
||||
baseline = stages["baseline-synthesis"]
|
||||
if baseline.get("environment"):
|
||||
required = ("initial_design", "pre_memory_design", "pre_sat_design", "post_sat_design", "mapped_design")
|
||||
files = [Path(baseline["environment"]["REPORTS"]) / f"{name}.json" for name in required]
|
||||
complete = baseline.get("status") == "passed" and all(p.is_file() for p in files)
|
||||
stages["storage-inventory"] = {**current, "status": "passed" if complete else "incomplete",
|
||||
"checkpoints": {p.stem: inventory(p) for p in files if p.is_file()},
|
||||
"missing_checkpoints": [p.stem for p in files if not p.is_file()],
|
||||
"candidate": "NOT_RUN (P1)", "whole_soc": "NOT_RUN"}
|
||||
dump(output / "storage-inventory.json", stages["storage-inventory"])
|
||||
if complete:
|
||||
baseline["metrics"] = synthesis_metrics(Path(baseline["environment"]["REPORTS"]))
|
||||
dump(output / "baseline-synthesis.json", baseline)
|
||||
passed = all(r.get("status") == "passed" for r in stages.values())
|
||||
dump(output / "p0-report.json", {**current, "status": "passed" if passed else "incomplete",
|
||||
"stages": stages, "pytest": "NOT_RUN (maintainer instruction)",
|
||||
"v2_lifecycle_and_hardware": "NOT_RUN (P1)", "physical_qualification": "NOT_RUN (P2)",
|
||||
"known_v1_limitations": ["ZEROIZED is command acknowledgement, not SRAM erase completion",
|
||||
"HAL AES_KEY_STATUS check uses the AES_STATUS mask", "Common FIFO flush retains payload"]})
|
||||
if not passed:
|
||||
raise ValueError("P0 evidence incomplete; see p0-report.json")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("stage", choices=("constants", "rtl", "synth", "report"))
|
||||
parser.add_argument("--variant-root", type=Path, required=True)
|
||||
parser.add_argument("--jobs", type=int, default=4)
|
||||
parser.add_argument("--case", choices=("crypto_vectors_p0_tb", "crypto_rsa2048_tb", "crypto_dma_p0_tb"))
|
||||
args = parser.parse_args()
|
||||
variant = args.variant_root.resolve()
|
||||
output = variant / "crypto/p0"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
if args.stage == "rtl":
|
||||
rtl(variant, output, args.jobs, args.case)
|
||||
else:
|
||||
{"constants": constants, "synth": synth, "report": report_all}[args.stage](variant, output)
|
||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||
parser.exit(1, f"CRYPTO-P0 {args.stage}: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -36,6 +36,14 @@ tooling.
|
||||
APB4 simulations; `test_crypto_register_parity.py` checks the handwritten
|
||||
RTL/C register ABI and `test_dma.py` covers DMA bursts, TCD fetch, CRC,
|
||||
tail-byte writes, and crypto endpoints.
|
||||
- `test_crypto_p0.py` checks the independent mathematical constant oracle,
|
||||
corruption rejection and hierarchy-multiplicity/register accounting.
|
||||
`make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS crypto-p0-rtl` runs the V1
|
||||
baseline without invoking Pytest: real Crypto/DMA channel 4/5 traffic,
|
||||
AES CBC/CTR tails, SHA-224/256 padding boundaries and RSA-2048 including
|
||||
equal-schedule private-result verification failure. The deterministic RSA
|
||||
key material is public test data. These are baseline measurements, not V2
|
||||
initialization/scrub or physical qualification.
|
||||
- `test_apu.py` checks the fail-closed APU APB4/IRQ shell plus the P2 private
|
||||
DMA, ring scheduler, stream router, Gateway A, and verification-only backend;
|
||||
`test_apu_register_parity.py` keeps its handwritten RTL/C ABI and matrix
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Independent mathematical CRYC1 oracle and deterministic PUBLIC RSA fixtures.
|
||||
|
||||
No production table, image packing helper, or DUT intermediate is imported.
|
||||
RSA fixtures are test material, never keys for real-world use.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import struct
|
||||
|
||||
|
||||
def gf_multiply(left: int, right: int) -> int:
|
||||
result = 0
|
||||
for _ in range(8):
|
||||
if right & 1:
|
||||
result ^= left
|
||||
left = ((left << 1) ^ (0x11B if left & 128 else 0)) & 255
|
||||
right >>= 1
|
||||
return result
|
||||
|
||||
|
||||
def sbox(value: int) -> int:
|
||||
inverse, power, exponent = 1, value, 254
|
||||
while exponent:
|
||||
if exponent & 1:
|
||||
inverse = gf_multiply(inverse, power)
|
||||
power = gf_multiply(power, power)
|
||||
exponent >>= 1
|
||||
if value == 0:
|
||||
inverse = 0
|
||||
result = inverse ^ 0x63
|
||||
for shift in range(1, 5):
|
||||
result ^= ((inverse << shift) | (inverse >> (8 - shift))) & 255
|
||||
return result
|
||||
|
||||
|
||||
def integer_root(number: int, degree: int) -> int:
|
||||
low, high = 0, 1 << ((number.bit_length() + degree - 1) // degree)
|
||||
while low + 1 < high:
|
||||
mid = (low + high) // 2
|
||||
if mid ** degree <= number:
|
||||
low = mid
|
||||
else:
|
||||
high = mid
|
||||
return low
|
||||
|
||||
|
||||
def constant_image() -> bytes:
|
||||
# Byte-oriented packing is independent of the production word-list packer.
|
||||
result = bytearray(8192)
|
||||
forward = [sbox(i) for i in range(256)]
|
||||
for index, value in enumerate(forward):
|
||||
result[index * 4] = value
|
||||
result[(256 + value) * 4] = index
|
||||
value = 1
|
||||
for index in range(1, 15):
|
||||
result[(512 + index) * 4] = value
|
||||
value = gf_multiply(value, 2)
|
||||
primes: list[int] = []
|
||||
candidate = 2
|
||||
while len(primes) < 64:
|
||||
if all(candidate % prime for prime in primes if prime * prime <= candidate):
|
||||
primes.append(candidate)
|
||||
candidate += 1
|
||||
for index, prime in enumerate(primes):
|
||||
struct.pack_into("<I", result, 4096 + index * 4,
|
||||
integer_root(prime << 96, 3) & 0xFFFFFFFF)
|
||||
for index in range(8):
|
||||
struct.pack_into("<I", result, 4096 + (64 + index) * 4,
|
||||
math.isqrt(primes[8 + index] << 128) & 0xFFFFFFFF)
|
||||
struct.pack_into("<I", result, 4096 + (72 + index) * 4,
|
||||
math.isqrt(primes[index] << 64) & 0xFFFFFFFF)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def probable_prime(value: int) -> bool:
|
||||
# Fixed bases for reproducible test construction, not a production key generator.
|
||||
bases = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53)
|
||||
if any(value % base == 0 for base in bases):
|
||||
return False
|
||||
odd, shift = value - 1, 0
|
||||
while odd % 2 == 0:
|
||||
odd //= 2
|
||||
shift += 1
|
||||
for base in bases:
|
||||
residue = pow(base, odd, value)
|
||||
if residue in (1, value - 1):
|
||||
continue
|
||||
for _ in range(shift - 1):
|
||||
residue = residue * residue % value
|
||||
if residue == value - 1:
|
||||
break
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def rsa_fixture() -> dict[str, int]:
|
||||
primes = []
|
||||
for label in (b"retroSoC CRYPTO-P0 p", b"retroSoC CRYPTO-P0 q"):
|
||||
candidate = int.from_bytes(hashlib.shake_256(label).digest(128), "big")
|
||||
candidate |= (3 << 1022) | 1
|
||||
while not probable_prime(candidate) or math.gcd(candidate - 1, 65537) != 1:
|
||||
candidate += 2
|
||||
primes.append(candidate)
|
||||
p, q = primes
|
||||
modulus = p * q
|
||||
private = pow(65537, -1, math.lcm(p - 1, q - 1))
|
||||
message = int.from_bytes(hashlib.shake_256(b"CRYPTO-P0 message").digest(255), "big")
|
||||
ciphertext = pow(message, 65537, modulus)
|
||||
if modulus.bit_length() != 2048 or pow(ciphertext, private, modulus) != message:
|
||||
raise ValueError("invalid deterministic RSA-2048 fixture")
|
||||
radix = 1 << 2048
|
||||
return {"modulus": modulus, "private": private, "message": message,
|
||||
"ciphertext": ciphertext, "n0_prime": -pow(modulus, -1, 1 << 32) % (1 << 32),
|
||||
"montgomery": (modulus - 1) ** 2 * pow(radix, -1, modulus) % modulus}
|
||||
@@ -0,0 +1,352 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
// Real DMA channels 4/5 -> AES ECB -> memory, with AXI stalls.
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module crypto_dma_p0_tb;
|
||||
import dma_pkg::*;
|
||||
|
||||
localparam int NumChannels = 8;
|
||||
localparam logic [31:0] SourceBase = 32'h4000_0000;
|
||||
localparam logic [31:0] DestinationBase = 32'h4000_1000;
|
||||
localparam logic [31:0] TransferBytes = 32'd64;
|
||||
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic [NumChannels*32-1:0] ch_cfg_i = '0;
|
||||
logic [NumChannels*32-1:0] src_addr_i = '0;
|
||||
logic [NumChannels*32-1:0] dst_addr_i = '0;
|
||||
logic [NumChannels*32-1:0] byte_count_i = '0;
|
||||
logic [NumChannels*32-1:0] request_sel_i = '0;
|
||||
logic [NumChannels*32-1:0] burst_cfg_i = '0;
|
||||
logic [ NumChannels-1:0] start_i = '0;
|
||||
logic [ NumChannels-1:0] busy_o;
|
||||
logic [ NumChannels-1:0] done_o;
|
||||
logic [ NumChannels-1:0] error_o;
|
||||
logic [NumChannels*32-1:0] bytes_done_o;
|
||||
logic [ 31:0] cycle_q = '0;
|
||||
logic read_active_q = 1'b0;
|
||||
logic [ 31:0] read_addr_q = '0;
|
||||
logic [ 4:0] read_beats_q = '0;
|
||||
logic [ 4:0] read_index_q = '0;
|
||||
logic write_active_q = 1'b0;
|
||||
logic [ 31:0] write_addr_q = '0;
|
||||
logic [ 4:0] write_beats_q = '0;
|
||||
logic [ 4:0] write_index_q = '0;
|
||||
logic write_response_q = 1'b0;
|
||||
logic stream_last_seen_q = 1'b0;
|
||||
integer write_count = 0;
|
||||
logic crypto_irq, crypto_input_proc, crypto_output_proc;
|
||||
apb4_if crypto_apb (
|
||||
.pclk (clk_i),
|
||||
.presetn(rst_n_i)
|
||||
);
|
||||
function automatic logic [31:0] plaintext(input logic [1:0] index);
|
||||
case (index)
|
||||
0: return 32'h33221100;
|
||||
1: return 32'h77665544;
|
||||
2: return 32'hbbaa9988;
|
||||
default: return 32'hffeeddcc;
|
||||
endcase
|
||||
endfunction
|
||||
function automatic logic [31:0] ciphertext(input logic [1:0] index);
|
||||
case (index)
|
||||
0: return 32'hd8e0c469;
|
||||
1: return 32'h30047b6a;
|
||||
2: return 32'h80b7cdd8;
|
||||
default: return 32'h5ac5b470;
|
||||
endcase
|
||||
endfunction
|
||||
task automatic apb_write(input logic [11:0] address, input logic [31:0] data);
|
||||
@(negedge clk_i);
|
||||
crypto_apb.paddr = {20'd0, address};
|
||||
crypto_apb.pwdata = data;
|
||||
crypto_apb.pstrb = 4'hf;
|
||||
crypto_apb.pwrite = 1'b1;
|
||||
crypto_apb.psel = 1'b1;
|
||||
crypto_apb.penable = 1'b0;
|
||||
@(negedge clk_i);
|
||||
crypto_apb.penable = 1'b1;
|
||||
do begin
|
||||
@(posedge clk_i);
|
||||
end while (!crypto_apb.pready);
|
||||
if (crypto_apb.pslverr) $fatal(1, "crypto DMA APB write %h", address);
|
||||
@(negedge clk_i);
|
||||
crypto_apb.psel = 1'b0;
|
||||
crypto_apb.penable = 1'b0;
|
||||
endtask
|
||||
task automatic apb_read(input logic [11:0] address, output logic [31:0] data);
|
||||
@(negedge clk_i);
|
||||
crypto_apb.paddr = {20'd0, address};
|
||||
crypto_apb.pstrb = 0;
|
||||
crypto_apb.pwrite = 1'b0;
|
||||
crypto_apb.psel = 1'b1;
|
||||
crypto_apb.penable = 1'b0;
|
||||
@(negedge clk_i);
|
||||
crypto_apb.penable = 1'b1;
|
||||
do begin
|
||||
@(posedge clk_i);
|
||||
end while (!crypto_apb.pready);
|
||||
if (crypto_apb.pslverr) $fatal(1, "crypto DMA APB read %h", address);
|
||||
data = crypto_apb.prdata;
|
||||
@(negedge clk_i);
|
||||
crypto_apb.psel = 1'b0;
|
||||
crypto_apb.penable = 1'b0;
|
||||
endtask
|
||||
initial begin
|
||||
#1000000;
|
||||
$fatal(1, "crypto DMA test watchdog");
|
||||
end
|
||||
|
||||
dma_req_if req ();
|
||||
axi4_if axi4 (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
axi4_stream_if i2s_tx_axis (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
axi4_stream_if i2s_rx_axis (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
axi4_stream_if dvp_rx_axis (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
axi4_stream_if crypto_in_axis (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
axi4_stream_if crypto_out_axis (
|
||||
.aclk (clk_i),
|
||||
.aresetn(rst_n_i)
|
||||
);
|
||||
|
||||
apb4_crypto u_crypto (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.irq_o (crypto_irq),
|
||||
.dma_input_proc_o (crypto_input_proc),
|
||||
.dma_output_proc_o(crypto_output_proc),
|
||||
.apb4 (crypto_apb),
|
||||
.crypto_in_axis (crypto_in_axis),
|
||||
.crypto_out_axis (crypto_out_axis)
|
||||
);
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
assign req.i2s_tx_proc = 1'b1;
|
||||
assign req.i2s_rx_proc = 1'b1;
|
||||
assign req.qspi_tx_proc = 1'b1;
|
||||
assign req.qspi_rx_proc = 1'b1;
|
||||
assign req.uart_tx_proc = 1'b1;
|
||||
assign req.uart_rx_proc = 1'b1;
|
||||
assign req.i2c0_tx_proc = 1'b1;
|
||||
assign req.i2c0_rx_proc = 1'b1;
|
||||
assign req.i2c1_tx_proc = 1'b1;
|
||||
assign req.i2c1_rx_proc = 1'b1;
|
||||
assign req.crypto_in_proc = crypto_input_proc;
|
||||
assign req.crypto_out_proc = crypto_output_proc;
|
||||
|
||||
assign axi4.arready = !read_active_q && cycle_q[0];
|
||||
assign axi4.rid = '0;
|
||||
assign axi4.rdata = plaintext(2'(((read_addr_q - SourceBase) >> 2) + read_index_q));
|
||||
assign axi4.rresp = `AXI4_RESP_OKAY;
|
||||
assign axi4.rlast = (read_index_q + 1'b1) == read_beats_q;
|
||||
assign axi4.ruser = '0;
|
||||
assign axi4.rvalid = read_active_q;
|
||||
assign axi4.awready = !write_active_q && !write_response_q && cycle_q[0];
|
||||
assign axi4.wready = write_active_q && cycle_q[1];
|
||||
assign axi4.bid = '0;
|
||||
assign axi4.bresp = `AXI4_RESP_OKAY;
|
||||
assign axi4.buser = '0;
|
||||
assign axi4.bvalid = write_response_q;
|
||||
|
||||
assign i2s_tx_axis.tready = 1'b0;
|
||||
assign i2s_rx_axis.tdata = '0;
|
||||
assign i2s_rx_axis.tkeep = '0;
|
||||
assign i2s_rx_axis.tstrb = '0;
|
||||
assign i2s_rx_axis.tlast = 1'b0;
|
||||
assign i2s_rx_axis.tid = '0;
|
||||
assign i2s_rx_axis.tdest = '0;
|
||||
assign i2s_rx_axis.tuser = '0;
|
||||
assign i2s_rx_axis.tvalid = 1'b0;
|
||||
assign dvp_rx_axis.tdata = '0;
|
||||
assign dvp_rx_axis.tkeep = '0;
|
||||
assign dvp_rx_axis.tstrb = '0;
|
||||
assign dvp_rx_axis.tlast = 1'b0;
|
||||
assign dvp_rx_axis.tid = '0;
|
||||
assign dvp_rx_axis.tdest = '0;
|
||||
assign dvp_rx_axis.tuser = '0;
|
||||
assign dvp_rx_axis.tvalid = 1'b0;
|
||||
|
||||
|
||||
dma_core #(
|
||||
.NumChannels (NumChannels),
|
||||
.MaxBurstBeats(16),
|
||||
.FifoDepth (16)
|
||||
) u_dma_core (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.global_reset_i (1'b0),
|
||||
.global_error_clear_i (1'b0),
|
||||
.ch_cfg_i (ch_cfg_i),
|
||||
.src_addr_i (src_addr_i),
|
||||
.dst_addr_i (dst_addr_i),
|
||||
.byte_count_i (byte_count_i),
|
||||
.request_sel_i (request_sel_i),
|
||||
.burst_cfg_i (burst_cfg_i),
|
||||
.start_i (start_i),
|
||||
.suspend_i ('0),
|
||||
.resume_i ('0),
|
||||
.abort_i ('0),
|
||||
.channel_reset_i ('0),
|
||||
.event_clear_i ('0),
|
||||
.busy_o (busy_o),
|
||||
.suspended_o (),
|
||||
.done_o (done_o),
|
||||
.aborted_o (),
|
||||
.error_o (error_o),
|
||||
.stream_last_o (),
|
||||
.event_status_o (),
|
||||
.error_status_o (),
|
||||
.error_addr_o (),
|
||||
.current_src_o (),
|
||||
.current_dst_o (),
|
||||
.remaining_o (),
|
||||
.bytes_done_o (bytes_done_o),
|
||||
.stall_cycles_lo_o (),
|
||||
.stall_cycles_hi_o (),
|
||||
.first_error_valid_o (),
|
||||
.first_error_channel_o(),
|
||||
.first_error_status_o (),
|
||||
.first_error_addr_hi_o(),
|
||||
.request_status_o (),
|
||||
.xpi_xfer_done_o (),
|
||||
.req (req),
|
||||
.axi4 (axi4),
|
||||
.i2s_tx_axis (i2s_tx_axis),
|
||||
.i2s_rx_axis (i2s_rx_axis),
|
||||
.dvp_rx_axis (dvp_rx_axis),
|
||||
.crypto_in_axis (crypto_in_axis),
|
||||
.crypto_out_axis (crypto_out_axis)
|
||||
);
|
||||
|
||||
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||
if (!rst_n_i) begin
|
||||
cycle_q <= '0;
|
||||
read_active_q <= 1'b0;
|
||||
write_active_q <= 1'b0;
|
||||
write_response_q <= 1'b0;
|
||||
stream_last_seen_q <= 1'b0;
|
||||
write_count <= 0;
|
||||
end else begin
|
||||
cycle_q <= cycle_q + 1'b1;
|
||||
if (axi4.arvalid && axi4.arready) begin
|
||||
read_active_q <= 1'b1;
|
||||
read_addr_q <= axi4.araddr;
|
||||
read_beats_q <= axi4.arlen + 1'b1;
|
||||
read_index_q <= '0;
|
||||
end
|
||||
if (axi4.rvalid && axi4.rready) begin
|
||||
if (axi4.rlast) begin
|
||||
read_active_q <= 1'b0;
|
||||
end else begin
|
||||
read_index_q <= read_index_q + 1'b1;
|
||||
end
|
||||
end
|
||||
if (axi4.awvalid && axi4.awready) begin
|
||||
write_active_q <= 1'b1;
|
||||
write_addr_q <= axi4.awaddr;
|
||||
write_beats_q <= axi4.awlen + 1'b1;
|
||||
write_index_q <= '0;
|
||||
end
|
||||
if (axi4.wvalid && axi4.wready) begin
|
||||
if ((write_addr_q + (write_index_q << 2)) != (DestinationBase + (write_count * 4))) begin
|
||||
$fatal(1, "crypto DMA write address mismatch");
|
||||
end
|
||||
if (axi4.wdata != ciphertext(2'(write_count))) begin
|
||||
$fatal(1, "crypto DMA write data mismatch: %h at %0d", axi4.wdata, write_count);
|
||||
end
|
||||
write_count <= write_count + 1;
|
||||
if (axi4.wlast) begin
|
||||
if ((write_index_q + 1'b1) != write_beats_q) begin
|
||||
$fatal(1, "crypto DMA WLAST mismatch");
|
||||
end
|
||||
write_active_q <= 1'b0;
|
||||
write_response_q <= 1'b1;
|
||||
end else begin
|
||||
write_index_q <= write_index_q + 1'b1;
|
||||
end
|
||||
end
|
||||
if (axi4.bvalid && axi4.bready) begin
|
||||
write_response_q <= 1'b0;
|
||||
end
|
||||
if (crypto_in_axis.tvalid && crypto_in_axis.tready && crypto_in_axis.tlast) begin
|
||||
stream_last_seen_q <= 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
initial begin
|
||||
integer wait_cycles;
|
||||
logic [31:0] status;
|
||||
crypto_apb.paddr = 0;
|
||||
crypto_apb.pprot = 0;
|
||||
crypto_apb.psel = 0;
|
||||
crypto_apb.penable = 0;
|
||||
crypto_apb.pwrite = 0;
|
||||
crypto_apb.pwdata = 0;
|
||||
crypto_apb.pstrb = 0;
|
||||
|
||||
ch_cfg_i[(4*32)+:32] = {22'd0, 2'd3, 1'b0, 1'b1, DMA_WIDTH_32, 1'b0, DMA_KIND_MM_TO_STREAM};
|
||||
src_addr_i[(4*32)+:32] = SourceBase;
|
||||
byte_count_i[(4*32)+:32] = TransferBytes;
|
||||
request_sel_i[(4*32)+:32] = {28'd0, DMA_REQUEST_CRYPTO_IN};
|
||||
burst_cfg_i[(4*32)+:32] = 32'd8;
|
||||
ch_cfg_i[(5*32)+:32] = {22'd0, 2'd3, 1'b1, 1'b0, DMA_WIDTH_32, 1'b0, DMA_KIND_STREAM_TO_MM};
|
||||
dst_addr_i[(5*32)+:32] = DestinationBase;
|
||||
byte_count_i[(5*32)+:32] = TransferBytes;
|
||||
request_sel_i[(5*32)+:32] = {28'd0, DMA_REQUEST_CRYPTO_OUT};
|
||||
burst_cfg_i[(5*32)+:32] = 32'd8;
|
||||
|
||||
repeat (3) @(posedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
apb_write(12'h104, 32'h100);
|
||||
apb_write(12'h140, 32'h03020100);
|
||||
apb_write(12'h144, 32'h07060504);
|
||||
apb_write(12'h148, 32'h0b0a0908);
|
||||
apb_write(12'h14c, 32'h0f0e0d0c);
|
||||
apb_write(12'h128, 32'h1);
|
||||
status = 0;
|
||||
while (!(status & 1)) apb_read(12'h12c, status);
|
||||
apb_write(12'h10c, TransferBytes);
|
||||
apb_write(12'h100, 32'h1);
|
||||
@(negedge clk_i);
|
||||
start_i[5:4] = 2'b11;
|
||||
@(negedge clk_i);
|
||||
start_i = '0;
|
||||
wait_cycles = 0;
|
||||
while (!(done_o[4] && done_o[5]) && (wait_cycles < 5000)) begin
|
||||
@(posedge clk_i);
|
||||
wait_cycles = wait_cycles + 1;
|
||||
end
|
||||
if (!(done_o[4] && done_o[5])) begin
|
||||
$fatal(1, "crypto DMA endpoint test timeout");
|
||||
end
|
||||
if ((error_o != '0) || (write_count != 16) || !stream_last_seen_q ||
|
||||
(bytes_done_o[(4*32)+:32] != TransferBytes) ||
|
||||
(bytes_done_o[(5*32)+:32] != TransferBytes)) begin
|
||||
$fatal(1, "crypto DMA result mismatch errors=%b writes=%0d last=%b", error_o, write_count,
|
||||
stream_last_seen_q);
|
||||
end
|
||||
apb_read(12'h108, status);
|
||||
if ((status & 32'h6) != 32'h2) $fatal(1, "AES DMA did not complete correctly");
|
||||
apb_read(12'h124, status);
|
||||
$display("CRYPTO_CYCLES operation=aes_dma_64bytes cycles=%0d", status);
|
||||
$display("CRYPTO_P0_DMA_PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module crypto_rsa2048_tb;
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic zeroize_i = 1'b0;
|
||||
logic abort_i = 1'b0;
|
||||
logic prepare_i = 1'b0;
|
||||
logic start_i = 1'b0;
|
||||
logic private_i = 1'b0;
|
||||
logic [11:0] exponent_bits_i = 12'd17;
|
||||
logic [2047:0] modulus_i, exponent_i, base_i;
|
||||
logic busy_o, done_o, error_o, prepared_o, result_valid_o;
|
||||
logic [2047:0] result_o;
|
||||
logic [31:0] cycles_o, progress_o;
|
||||
logic mont_start = 1'b0;
|
||||
logic mont_busy, mont_done;
|
||||
logic [2047:0] mont_result;
|
||||
logic [2047:0] vectors [0:5];
|
||||
logic [ 31:0] private_cycles;
|
||||
string vector_path;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
crypto_rsa_core #(
|
||||
.Bits(2048)
|
||||
) u_rsa (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.zeroize_i (zeroize_i),
|
||||
.abort_i (abort_i),
|
||||
.prepare_i (prepare_i),
|
||||
.start_i (start_i),
|
||||
.private_i (private_i),
|
||||
.exponent_bits_i(exponent_bits_i),
|
||||
.modulus_i (modulus_i),
|
||||
.exponent_i (exponent_i),
|
||||
.base_i (base_i),
|
||||
.busy_o (busy_o),
|
||||
.done_o (done_o),
|
||||
.error_o (error_o),
|
||||
.prepared_o (prepared_o),
|
||||
.result_valid_o (result_valid_o),
|
||||
.result_o (result_o),
|
||||
.cycles_o (cycles_o),
|
||||
.progress_o (progress_o)
|
||||
);
|
||||
crypto_montgomery #(
|
||||
.Bits(2048)
|
||||
) u_mont (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.zeroize_i (zeroize_i),
|
||||
.start_i (mont_start),
|
||||
.left_i (modulus_i - 2048'd1),
|
||||
.right_i (modulus_i - 2048'd1),
|
||||
.modulus_i (modulus_i),
|
||||
.n0_prime_i(vectors[4][31:0]),
|
||||
.busy_o (mont_busy),
|
||||
.done_o (mont_done),
|
||||
.result_o (mont_result)
|
||||
);
|
||||
|
||||
task automatic run_operation(input logic private_mode, input logic expect_error);
|
||||
@(negedge clk_i);
|
||||
private_i = private_mode;
|
||||
start_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
start_i = 1'b0;
|
||||
wait (done_o);
|
||||
#1;
|
||||
if (error_o != expect_error || result_valid_o == expect_error)
|
||||
$fatal(1, "RSA-2048 completion/verification mismatch");
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
#2000000000;
|
||||
$fatal(1, "RSA-2048 test watchdog");
|
||||
end
|
||||
|
||||
initial begin
|
||||
if (!$value$plusargs("vectors=%s", vector_path)) $fatal(1, "missing RSA vectors");
|
||||
$readmemh(vector_path, vectors);
|
||||
modulus_i = vectors[0];
|
||||
exponent_i = 2048'd65537;
|
||||
base_i = vectors[2];
|
||||
repeat (3) @(negedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
prepare_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
prepare_i = 1'b0;
|
||||
wait (done_o);
|
||||
#1;
|
||||
if (!prepared_o || error_o) $fatal(1, "RSA-2048 prepare");
|
||||
$display("CRYPTO_CYCLES operation=rsa2048_prepare cycles=%0d", cycles_o);
|
||||
|
||||
@(negedge clk_i);
|
||||
mont_start = 1'b1;
|
||||
@(negedge clk_i);
|
||||
mont_start = 1'b0;
|
||||
wait (mont_done);
|
||||
#1;
|
||||
if (mont_result !== vectors[5]) $fatal(1, "RSA-2048 Montgomery carry vector");
|
||||
|
||||
run_operation(1'b0, 1'b0);
|
||||
if (result_o !== vectors[3]) $fatal(1, "RSA-2048 public numerical mismatch");
|
||||
$display("CRYPTO_CYCLES operation=rsa2048_public cycles=%0d", cycles_o);
|
||||
exponent_i = vectors[1];
|
||||
exponent_bits_i = 12'd2048;
|
||||
base_i = vectors[3];
|
||||
run_operation(1'b1, 1'b0);
|
||||
if (result_o !== vectors[2]) $fatal(1, "RSA-2048 private numerical mismatch");
|
||||
private_cycles = cycles_o;
|
||||
$display("CRYPTO_CYCLES operation=rsa2048_private cycles=%0d", cycles_o);
|
||||
|
||||
exponent_i = vectors[1] ^ 2048'd2;
|
||||
run_operation(1'b1, 1'b1);
|
||||
if (cycles_o != private_cycles) $fatal(1, "private exponent changed schedule");
|
||||
if (result_o != 2048'd0) $fatal(1, "failed private verification released data");
|
||||
$display("CRYPTO_CYCLES operation=rsa2048_bad_private cycles=%0d", cycles_o);
|
||||
|
||||
@(negedge clk_i);
|
||||
zeroize_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
zeroize_i = 1'b0;
|
||||
if (prepared_o || result_valid_o || busy_o || result_o != 0)
|
||||
$fatal(1, "RSA-2048 zeroize state");
|
||||
$display("CRYPTO_P0_RSA2048_PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||
// SPDX-License-Identifier: MulanPSL-2.0
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
module crypto_vectors_p0_tb;
|
||||
import crypto_pkg::*;
|
||||
logic [255:0] expected_sha[0:21];
|
||||
string sha_path;
|
||||
initial begin
|
||||
#1000000;
|
||||
$fatal(1, "Crypto vector watchdog");
|
||||
end
|
||||
|
||||
logic clk_i = 1'b0;
|
||||
logic rst_n_i = 1'b0;
|
||||
logic zeroize_i = 1'b0;
|
||||
|
||||
logic aes_abort;
|
||||
logic aes_key_commit;
|
||||
logic [ 1:0] aes_key_size;
|
||||
logic [255:0] aes_key;
|
||||
logic aes_key_valid;
|
||||
logic aes_key_busy;
|
||||
logic aes_start;
|
||||
logic [ 1:0] aes_mode;
|
||||
logic aes_decrypt;
|
||||
logic [ 31:0] aes_length;
|
||||
logic [127:0] aes_iv;
|
||||
logic aes_input_valid;
|
||||
logic aes_input_ready;
|
||||
logic [ 31:0] aes_input_data;
|
||||
logic [ 3:0] aes_input_keep;
|
||||
logic aes_input_last;
|
||||
logic aes_output_valid;
|
||||
logic aes_output_ready;
|
||||
logic [ 31:0] aes_output_data;
|
||||
logic [ 3:0] aes_output_keep;
|
||||
logic aes_output_last;
|
||||
logic aes_busy;
|
||||
logic aes_done;
|
||||
logic aes_error;
|
||||
logic [ 31:0] aes_bytes_in;
|
||||
logic [ 31:0] aes_bytes_out;
|
||||
logic [ 31:0] aes_cycles;
|
||||
logic [127:0] aes_chain;
|
||||
|
||||
logic sha_abort;
|
||||
logic sha_start;
|
||||
logic sha256;
|
||||
logic [ 63:0] sha_length;
|
||||
logic sha_input_valid;
|
||||
logic sha_input_ready;
|
||||
logic [ 31:0] sha_input_data;
|
||||
logic [ 3:0] sha_input_keep;
|
||||
logic sha_input_last;
|
||||
logic sha_busy;
|
||||
logic sha_done;
|
||||
logic sha_error;
|
||||
logic sha_digest_valid;
|
||||
logic [255:0] sha_digest;
|
||||
logic [ 63:0] sha_bytes_in;
|
||||
logic [ 31:0] sha_cycles;
|
||||
|
||||
always #5 clk_i = ~clk_i;
|
||||
|
||||
crypto_aes_engine u_crypto_aes_engine (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.zeroize_i (zeroize_i),
|
||||
.abort_i (aes_abort),
|
||||
.key_commit_i (aes_key_commit),
|
||||
.key_size_i (aes_key_size),
|
||||
.key_i (aes_key),
|
||||
.key_valid_o (aes_key_valid),
|
||||
.key_busy_o (aes_key_busy),
|
||||
.start_i (aes_start),
|
||||
.mode_i (aes_mode),
|
||||
.decrypt_i (aes_decrypt),
|
||||
.length_i (aes_length),
|
||||
.iv_i (aes_iv),
|
||||
.input_valid_i (aes_input_valid),
|
||||
.input_ready_o (aes_input_ready),
|
||||
.input_data_i (aes_input_data),
|
||||
.input_keep_i (aes_input_keep),
|
||||
.input_last_i (aes_input_last),
|
||||
.output_valid_o(aes_output_valid),
|
||||
.output_ready_i(aes_output_ready),
|
||||
.output_data_o (aes_output_data),
|
||||
.output_keep_o (aes_output_keep),
|
||||
.output_last_o (aes_output_last),
|
||||
.busy_o (aes_busy),
|
||||
.done_o (aes_done),
|
||||
.error_o (aes_error),
|
||||
.bytes_in_o (aes_bytes_in),
|
||||
.bytes_out_o (aes_bytes_out),
|
||||
.cycles_o (aes_cycles),
|
||||
.chain_o (aes_chain)
|
||||
);
|
||||
|
||||
crypto_sha2_engine u_crypto_sha2_engine (
|
||||
.clk_i (clk_i),
|
||||
.rst_n_i (rst_n_i),
|
||||
.zeroize_i (zeroize_i),
|
||||
.abort_i (sha_abort),
|
||||
.start_i (sha_start),
|
||||
.sha256_i (sha256),
|
||||
.length_i (sha_length),
|
||||
.input_valid_i (sha_input_valid),
|
||||
.input_ready_o (sha_input_ready),
|
||||
.input_data_i (sha_input_data),
|
||||
.input_keep_i (sha_input_keep),
|
||||
.input_last_i (sha_input_last),
|
||||
.busy_o (sha_busy),
|
||||
.done_o (sha_done),
|
||||
.error_o (sha_error),
|
||||
.digest_valid_o(sha_digest_valid),
|
||||
.digest_o (sha_digest),
|
||||
.bytes_in_o (sha_bytes_in),
|
||||
.cycles_o (sha_cycles)
|
||||
);
|
||||
|
||||
task automatic aes_load_key(input logic [255:0] key);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
aes_key_size = AES_KEY_128;
|
||||
aes_key = key;
|
||||
aes_key_commit = 1'b1;
|
||||
@(negedge clk_i);
|
||||
aes_key_commit = 1'b0;
|
||||
wait (aes_key_valid);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic aes_begin(input logic [1:0] mode, input logic [31:0] length,
|
||||
input logic [127:0] iv);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
aes_mode = mode;
|
||||
aes_length = length;
|
||||
aes_iv = iv;
|
||||
aes_decrypt = 1'b0;
|
||||
aes_start = 1'b1;
|
||||
@(negedge clk_i);
|
||||
aes_start = 1'b0;
|
||||
wait (aes_busy);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic aes_send(input logic [31:0] data, input logic [3:0] keep, input logic last);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
aes_input_data = data;
|
||||
aes_input_keep = keep;
|
||||
aes_input_last = last;
|
||||
aes_input_valid = 1'b1;
|
||||
while (!aes_input_ready) begin
|
||||
@(negedge clk_i);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
aes_input_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic aes_expect(input logic [31:0] data, input logic [3:0] keep, input logic last);
|
||||
begin
|
||||
aes_output_ready = 1'b1;
|
||||
while (!aes_output_valid) begin
|
||||
@(negedge clk_i);
|
||||
end
|
||||
if ((aes_output_data != data) || (aes_output_keep != keep) || (aes_output_last != last)) begin
|
||||
$fatal(1, "AES stream mismatch: got %h/%h/%b expected %h/%h/%b", aes_output_data,
|
||||
aes_output_keep, aes_output_last, data, keep, last);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
aes_output_ready = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic sha_begin(input logic use_sha256, input logic [63:0] length);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
sha256 = use_sha256;
|
||||
sha_length = length;
|
||||
sha_start = 1'b1;
|
||||
@(negedge clk_i);
|
||||
sha_start = 1'b0;
|
||||
wait (sha_busy);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic sha_send(input logic [31:0] data, input logic [3:0] keep, input logic last);
|
||||
begin
|
||||
@(negedge clk_i);
|
||||
sha_input_data = data;
|
||||
sha_input_keep = keep;
|
||||
sha_input_last = last;
|
||||
sha_input_valid = 1'b1;
|
||||
while (!sha_input_ready) begin
|
||||
@(negedge clk_i);
|
||||
end
|
||||
@(negedge clk_i);
|
||||
sha_input_valid = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
aes_abort = 1'b0;
|
||||
aes_key_commit = 1'b0;
|
||||
aes_key_size = AES_KEY_128;
|
||||
aes_key = '0;
|
||||
aes_start = 1'b0;
|
||||
aes_mode = AES_MODE_ECB;
|
||||
aes_decrypt = 1'b0;
|
||||
aes_length = '0;
|
||||
aes_iv = '0;
|
||||
aes_input_valid = 1'b0;
|
||||
aes_input_data = '0;
|
||||
aes_input_keep = '0;
|
||||
aes_input_last = 1'b0;
|
||||
aes_output_ready = 1'b0;
|
||||
sha_abort = 1'b0;
|
||||
sha_start = 1'b0;
|
||||
sha256 = 1'b1;
|
||||
sha_length = '0;
|
||||
sha_input_valid = 1'b0;
|
||||
sha_input_data = '0;
|
||||
sha_input_keep = '0;
|
||||
sha_input_last = 1'b0;
|
||||
|
||||
repeat (3) @(posedge clk_i);
|
||||
rst_n_i = 1'b1;
|
||||
|
||||
if (!$value$plusargs("sha=%s", sha_path)) $fatal(1, "missing SHA oracle vectors");
|
||||
$readmemh(sha_path, expected_sha);
|
||||
// NIST SP 800-38A F.2.1 first CBC block, with a stalled output.
|
||||
aes_load_key(256'h2b7e151628aed2a6abf7158809cf4f3c_00000000000000000000000000000000);
|
||||
aes_begin(AES_MODE_CBC, 32'd16, 128'h000102030405060708090a0b0c0d0e0f);
|
||||
aes_send(32'he2bec16b, 4'hf, 1'b0);
|
||||
aes_send(32'h969f402e, 4'hf, 1'b0);
|
||||
aes_send(32'h117e3de9, 4'hf, 1'b0);
|
||||
aes_send(32'h2a179373, 4'hf, 1'b1);
|
||||
wait (aes_output_valid);
|
||||
repeat (7) begin
|
||||
@(negedge clk_i);
|
||||
if (!aes_output_valid || aes_output_data != 32'hacab4976)
|
||||
$fatal(1, "AES output changed while stalled");
|
||||
end
|
||||
aes_expect(32'hacab4976, 4'hf, 1'b0);
|
||||
aes_expect(32'h46b21981, 4'hf, 1'b0);
|
||||
aes_expect(32'h9b8ee9ce, 4'hf, 1'b0);
|
||||
aes_expect(32'h7d19e912, 4'hf, 1'b1);
|
||||
wait (aes_done);
|
||||
aes_begin(AES_MODE_CTR, 32'd3, 128'hf0f1f2f3f4f5f6f7f8f9fafbfcfdfeff);
|
||||
aes_send(32'h00bec16b, 4'h7, 1'b1);
|
||||
// Only KEEP-selected bytes are meaningful on a partial output word.
|
||||
aes_output_ready = 1'b1;
|
||||
wait (aes_output_valid);
|
||||
#1;
|
||||
if (aes_output_data[23:0] != 24'h614d87 || aes_output_keep != 4'h7 || !aes_output_last)
|
||||
$fatal(
|
||||
1,
|
||||
"AES partial CTR tail data=%h keep=%h last=%b",
|
||||
aes_output_data,
|
||||
aes_output_keep,
|
||||
aes_output_last
|
||||
);
|
||||
@(posedge clk_i);
|
||||
@(negedge clk_i);
|
||||
aes_output_ready = 1'b0;
|
||||
wait (aes_done);
|
||||
|
||||
for (int mode = 0; mode < 2; mode++) begin
|
||||
for (int test_id = 0; test_id < 11; test_id++) begin
|
||||
int length;
|
||||
case (test_id)
|
||||
0: length = 0;
|
||||
1: length = 1;
|
||||
2: length = 3;
|
||||
3: length = 55;
|
||||
4: length = 56;
|
||||
5: length = 63;
|
||||
6: length = 64;
|
||||
7: length = 65;
|
||||
8: length = 127;
|
||||
9: length = 128;
|
||||
default: length = 129;
|
||||
endcase
|
||||
sha_begin(1'(mode), 64'(length));
|
||||
for (int offset = 0; offset < length; offset += 4) begin
|
||||
logic [31:0] data;
|
||||
logic [ 3:0] keep;
|
||||
data = 0;
|
||||
keep = 0;
|
||||
for (int byte_id = 0; byte_id < 4; byte_id++) begin
|
||||
if (offset + byte_id < length) begin
|
||||
data[byte_id*8+:8] = 8'(offset + byte_id);
|
||||
keep[byte_id] = 1'b1;
|
||||
end
|
||||
end
|
||||
sha_send(data, keep, offset + 4 >= length);
|
||||
end
|
||||
wait (sha_done);
|
||||
#1;
|
||||
if (sha_error || !sha_digest_valid || sha_digest !== expected_sha[mode*11+test_id])
|
||||
$fatal(1, "SHA padding mismatch mode=%0d bytes=%0d", mode, length);
|
||||
$display("CRYPTO_SHA_CASE mode=%0d bytes=%0d cycles=%0d", mode, length, sha_cycles);
|
||||
end
|
||||
end
|
||||
// Abort a partially supplied SHA job, then invalidate both engines.
|
||||
sha_begin(1'b1, 64'd64);
|
||||
sha_send(32'h01020304, 4'hf, 1'b0);
|
||||
@(negedge clk_i);
|
||||
sha_abort = 1'b1;
|
||||
@(negedge clk_i);
|
||||
sha_abort = 1'b0;
|
||||
if (sha_busy || sha_digest_valid) $fatal(1, "SHA abort retained validity");
|
||||
zeroize_i = 1'b1;
|
||||
@(negedge clk_i);
|
||||
zeroize_i = 1'b0;
|
||||
if (aes_key_valid || aes_busy || sha_digest_valid) $fatal(1, "V1 zeroize validity");
|
||||
$display("CRYPTO_P0_VECTORS_PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Independent image and hierarchy-accounting regressions for CRYPTO-P0."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crypto_reference import constant_image, integer_root
|
||||
from scripts.crypto_constants import pack, validate
|
||||
from scripts.crypto_p0 import inventory
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_cryc1_independent_mathematical_image():
|
||||
assert pack(ROOT) == constant_image()
|
||||
assert validate(constant_image())["bytes"] == 8192
|
||||
|
||||
|
||||
@pytest.mark.parametrize("offset", (0, 1023, 2048, 4095, 4096, 8191))
|
||||
def test_cryc1_rejects_data_and_padding_corruption(offset):
|
||||
image = bytearray(constant_image())
|
||||
image[offset] ^= 1
|
||||
with pytest.raises(ValueError, match="frozen identity"):
|
||||
validate(bytes(image))
|
||||
|
||||
|
||||
def test_integer_root_exact_boundaries():
|
||||
for degree in (2, 3):
|
||||
for value in (1, 2, 255, 1 << 32):
|
||||
assert integer_root(value ** degree, degree) == value
|
||||
assert integer_root(value ** degree - 1, degree) == value - 1
|
||||
|
||||
|
||||
def test_inventory_counts_reused_modules_and_separate_registers(tmp_path):
|
||||
module = {"memories": {"ram": {"width": 32, "size": 64},
|
||||
"rom": {"width": 8, "size": 256}},
|
||||
"cells": {"write": {"type": "$memwr_v2", "parameters": {"MEMID": "\\ram"}},
|
||||
"ff": {"type": "$adffe", "parameters": {"WIDTH": "100000000000"}}}}
|
||||
design = {"modules": {"apb4_crypto": {"cells": {
|
||||
"one": {"type": "shared"}, "two": {"type": "shared"}}}, "shared": module}}
|
||||
path = tmp_path / "design.json"
|
||||
path.write_text(json.dumps(design))
|
||||
report = inventory(path)
|
||||
assert report["ram_bits"] == 4096
|
||||
assert report["rom_bits"] == 4096
|
||||
assert report["register_bits"] == 4096
|
||||
assert len(report["rows"]) == 3
|
||||
|
||||
|
||||
def test_inventory_rejects_recursive_hierarchy(tmp_path):
|
||||
path = tmp_path / "design.json"
|
||||
path.write_text(json.dumps({"modules": {"apb4_crypto": {
|
||||
"cells": {"loop": {"type": "apb4_crypto"}}}}}))
|
||||
with pytest.raises(ValueError, match="recursive"):
|
||||
inventory(path)
|
||||
Reference in new issue
Block a user