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:
maksyuki committed 2026-09-25 11:21:42 +08:00
1 parent 92830d963d
commit b72e14ea40
16 files changed
+2183 -21

No files matched your search

+14
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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"
+9
View File
@@ -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
+21
View File
@@ -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).
+86
View File
@@ -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()
+445
View File
@@ -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()
+8
View File
@@ -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
+118
View File
@@ -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}
+352
View File
@@ -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
+134
View File
@@ -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
+324
View File
@@ -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
+57
View File
@@ -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)