From b72e14ea40d436abe6bd89b1df663acd916e8c37 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Fri, 25 Sep 2026 11:21:42 +0800 Subject: [PATCH] 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. --- Makefile | 14 + docs/README.md | 5 +- docs/ip/README.md | 4 + docs/ip/crypto.md | 591 +++++++++++++++++- physical/smoke/syn/README.md | 9 + .../syn/yosys/script/crypto_block_synth.tcl | 27 + rtl/README.md | 9 + scripts/README.md | 21 + scripts/crypto_constants.py | 86 +++ scripts/crypto_p0.py | 445 +++++++++++++ tests/README.md | 8 + tests/crypto_reference.py | 118 ++++ tests/rtl/crypto_dma_p0_tb.sv | 352 +++++++++++ tests/rtl/crypto_rsa2048_tb.sv | 134 ++++ tests/rtl/crypto_vectors_p0_tb.sv | 324 ++++++++++ tests/test_crypto_p0.py | 57 ++ 16 files changed, 2183 insertions(+), 21 deletions(-) create mode 100644 physical/smoke/syn/yosys/script/crypto_block_synth.tcl create mode 100644 scripts/crypto_constants.py create mode 100644 scripts/crypto_p0.py create mode 100644 tests/crypto_reference.py create mode 100644 tests/rtl/crypto_dma_p0_tb.sv create mode 100644 tests/rtl/crypto_rsa2048_tb.sv create mode 100644 tests/rtl/crypto_vectors_p0_tb.sv create mode 100644 tests/test_crypto_p0.py diff --git a/Makefile b/Makefile index 53a51482..9df0f85b 100644 --- a/Makefile +++ b/Makefile @@ -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' \ diff --git a/docs/README.md b/docs/README.md index b1a1e98b..47a6a022 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/ip/README.md b/docs/ip/README.md index c27d97d2..8af7ebef 100644 --- a/docs/ip/README.md +++ b/docs/ip/README.md @@ -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. diff --git a/docs/ip/crypto.md b/docs/ip/crypto.md index 3c3b8241..73a4e170 100644 --- a/docs/ip/crypto.md +++ b/docs/ip/crypto.md @@ -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//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 diff --git a/physical/smoke/syn/README.md b/physical/smoke/syn/README.md index 7db4ff57..352dc81a 100644 --- a/physical/smoke/syn/README.md +++ b/physical/smoke/syn/README.md @@ -46,6 +46,15 @@ The default ABC target period is derived from the `external` STA period in `YOSYS_TARGET_PERIOD_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//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. diff --git a/physical/smoke/syn/yosys/script/crypto_block_synth.tcl b/physical/smoke/syn/yosys/script/crypto_block_synth.tcl new file mode 100644 index 00000000..35758797 --- /dev/null +++ b/physical/smoke/syn/yosys/script/crypto_block_synth.tcl @@ -0,0 +1,27 @@ +# Copyright (c) 2026 Yuchi Miao +# 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" diff --git a/rtl/README.md b/rtl/README.md index 5968fc2c..276c63a1 100644 --- a/rtl/README.md +++ b/rtl/README.md @@ -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 diff --git a/scripts/README.md b/scripts/README.md index d1cc22f2..b7205801 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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//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). diff --git a/scripts/crypto_constants.py b/scripts/crypto_constants.py new file mode 100644 index 00000000..eb244718 --- /dev/null +++ b/scripts/crypto_constants.py @@ -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(" 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() diff --git a/tests/README.md b/tests/README.md index f9afef71..1858c96a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 diff --git a/tests/crypto_reference.py b/tests/crypto_reference.py new file mode 100644 index 00000000..23e7332a --- /dev/null +++ b/tests/crypto_reference.py @@ -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(" 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} diff --git a/tests/rtl/crypto_dma_p0_tb.sv b/tests/rtl/crypto_dma_p0_tb.sv new file mode 100644 index 00000000..6bc091cd --- /dev/null +++ b/tests/rtl/crypto_dma_p0_tb.sv @@ -0,0 +1,352 @@ +// Copyright (c) 2026 Yuchi Miao +// 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 diff --git a/tests/rtl/crypto_rsa2048_tb.sv b/tests/rtl/crypto_rsa2048_tb.sv new file mode 100644 index 00000000..acf79502 --- /dev/null +++ b/tests/rtl/crypto_rsa2048_tb.sv @@ -0,0 +1,134 @@ +// Copyright (c) 2026 Yuchi Miao +// 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 diff --git a/tests/rtl/crypto_vectors_p0_tb.sv b/tests/rtl/crypto_vectors_p0_tb.sv new file mode 100644 index 00000000..af2b9f7b --- /dev/null +++ b/tests/rtl/crypto_vectors_p0_tb.sv @@ -0,0 +1,324 @@ +// Copyright (c) 2026 Yuchi Miao +// 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 diff --git a/tests/test_crypto_p0.py b/tests/test_crypto_p0.py new file mode 100644 index 00000000..b8103431 --- /dev/null +++ b/tests/test_crypto_p0.py @@ -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)