Skip to content

[crypto/otbn/silicon_creator, mldsa] Optimize public DMEM write in ML-DSA verify - #31365

Open
GillonB wants to merge 4 commits into
lowRISC:masterfrom
GillonB:mldsa_verify_dmem_fast_opt
Open

GillonB wants to merge 4 commits into
lowRISC:masterfrom
GillonB:mldsa_verify_dmem_fast_opt

Conversation

@GillonB

@GillonB GillonB commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

ML-DSA-87 signature verification was previously bottlenecked by host CPU data marshalling over TL-UL MMIO, taking 568,648 cycles (~3.79 ms @ 150 MHz) to write the 1,821-word public input buffer (pk, sig, mu) due to side-channel Fisher-Yates PRNG random-order permutation and per-word software CRC-32 checksum calculation.

Because signature verification operates exclusively on public data, side-channel shuffling and software CRC verification on input writes provide no cryptographic protection and are unnecessary overhead.

This change:

  1. Implements otbn_dmem_write_public() in the OTBN driver to perform linear 32-bit MMIO store transfers with hardened boundary and loop iteration checks.
  2. Updates mldsa87_verify_internal_start() to use otbn_dmem_write_public() for public key, signature, and message digest transfers.

This reduces DMEM write latency from 568,648 cycles to 23,763 cycles (23.9x speedup) and enables ML-DSA-87 verification with a cached/preloaded OTBN app image to complete in 2.29 ms on 150 MHz silicon.

@GillonB
GillonB requested a review from a team as a code owner September 17, 2026 13:25
@GillonB
GillonB requested review from engdoreis and removed request for a team September 17, 2026 13:25
@GillonB

GillonB commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

1. Summary & Motivation

ML-DSA-87 signature verification on OpenTitan was previously measured at ~8.60 ms on Gen 2 ASIC (150 MHz), exceeding the target performance budget of $\le 4.00\text{ ms}$ (numbers converted from FPGA measurements)

Hardware profiling on the ChipWhisperer CW340 FPGA showed that the pure OTBN coprocessor computation is only 2.13 ms (318,789 cycles). The primary latency bottleneck was host CPU (Ibex) data marshalling over TileLink-Uncached-Lite (TL-UL) MMIO:

  • otbn_dmem_write() enforces side-channel countermeasures (Fisher-Yates random order permutation via PRNG + per-word software CRC-32 calculation matching LOAD_CHECKSUM).
  • Transferring the verification inputs ($pk = 2,592\text{ B}$, $\sigma = 4,628\text{ B}$, $\mu = 64\text{ B}$ — total 1,821 words) took 568,648 cycles (~3.79 ms @ 150 MHz) at ~312 cycles/word, consuming 44% of total verify runtime.

This PR introduces otbn_dmem_write_public() to write non-secret data linearly over MMIO, reducing DMEM write latency by $23.9\times$ and bringing end-to-end cached verification down to 2.29 ms.


2. Security Analysis

  • Side-Channel Analysis (SCA / DPA):
    • In signature verification (FIPS 204), all inputs ($pk, \sigma, \mu$) and intermediate variables are public parameters.
    • No secret keys or masked shares are present in memory. Power/EM traces captured during public input transfers leak only publicly known data, introducing zero side-channel vulnerability.
  • Fault Injection (FI) & Integrity (Fail-Safe):
    • In otbn_dmem_write_public(), software CRC-32 tracking against LOAD_CHECKSUM is omitted.
    • If a fault corrupts $pk$, $\sigma$, or $\mu$ in DMEM during transfer, the signature verification arithmetic ($\mathbf{w}' = \mathbf{A}\mathbf{z} - c\mathbf{t}_1 \cdot 2^d$) will produce a diverging challenge hash ($c' \neq \tilde{c}$). The signature verification mathematically fails closed/safe with overwhelming probability ($1 - 2^{-256}$).
    • Verification pass/fail decisions remain protected by OTBN's 32-bit sparse-FSM return status code (0x7baf73d2 vs 0xadf1aebd, Hamming distance 21) and host-side hardened_memeq.
  • Memory Safety & Loop Hardening (CFI):
    • Strict bounds checking (check_offset_len(dest, num_words, kOtbnDMemSizeBytes)) is fully preserved.
    • Ibex glitch hardening is retained via launder32(i), HARDENED_CHECK_LT(i, num_words), and post-loop HARDENED_CHECK_EQ(i, num_words).
  • API Scoping:
    • otbn_dmem_write_public() is explicitly named and scoped for public verification inputs only. Private key handling, signing, and key generation routines strictly continue using otbn_dmem_write().

3. Empirical Hardware Measurements (ChipWhisperer CW340 FPGA)

A. Step-by-Step Cycle Breakdown (1,821 words / 7,284 bytes input)

Operation Phase Baseline (otbn_dmem_write) Optimized (otbn_dmem_write_public) Optimized + Cached App Image
1. Application Load (app_load) 401,870 cycles 401,870 cycles 0 cycles (reused in IMEM)
2. DMEM Input Write (dmem_wr) 568,648 cycles 23,763 cycles ($23.9\times$) 23,763 cycles
3. Pure OTBN Core Exec (pure_exec) 318,794 cycles 318,789 cycles 318,789 cycles
4. Host Result Readback (read) 1,070 cycles 1,070 cycles 1,070 cycles
Total Verify Routine 1,290,382 cycles 745,372 cycles 343,498 cycles

B. Latency Comparison across Target Clocks

Configuration Mode FPGA (24 MHz) Gen 1 ASIC (100 MHz) Gen 2 ASIC (150 MHz)
1. Baseline (Hardened Write + App Load) $53.77\text{ ms}$ $12.90\text{ ms}$ $8.60\text{ ms}$
2. Optimized (Fast Write + App Load) $31.06\text{ ms}$ $7.45\text{ ms}$ $4.97\text{ ms}$
3. Optimized (Fast Write + Pre-loaded App) $14.31\text{ ms}$ $3.43\text{ ms}$ 2.29 ms
Pure OTBN Hardware Core Execution Only $13.28\text{ ms}$ $3.19\text{ ms}$ 2.13 ms

4. Changes Included

  1. sw/device/lib/crypto/drivers/otbn.h: Declared otbn_dmem_write_public() with documentation specifying its use for public, non-secret parameters.
  2. sw/device/lib/crypto/drivers/otbn.c: Implemented otbn_dmem_write_public() using a linear store loop with hardened loop bounds and address checks.
  3. sw/device/lib/crypto/impl/mldsa/mldsa.c: Updated mldsa87_verify_internal_start() to transfer $pk$, $\sigma$, and $\mu$ using otbn_dmem_write_public().

@GillonB GillonB changed the title [crypto/otbn, mldsa] Optimize public DMEM write in ML-DSA verify [crypto/otbn/silicon_creator, mldsa] Optimize public DMEM write in ML-DSA verify Sep 17, 2026
@GillonB GillonB added the CI:Rerun Rerun failed CI jobs label Sep 17, 2026
@github-actions github-actions Bot removed the CI:Rerun Rerun failed CI jobs label Sep 17, 2026
@GillonB
GillonB force-pushed the mldsa_verify_dmem_fast_opt branch from a3c79f0 to 3d618b0 Compare September 17, 2026 14:35

@andrea-caforio andrea-caforio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @GillonB, that's a crazy speedup. :-) How much does it degrade if you add back the CRC?

@GillonB
GillonB force-pushed the mldsa_verify_dmem_fast_opt branch from 3d618b0 to e2133e6 Compare September 17, 2026 15:35
@GillonB

GillonB commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

@andrea-caforio Still getting decent gains with the CRC:

  • ML-DSA-87 verify: from 39.92 ms to 16.25 ms on FPGA at 25 MHz -> 6.39 ms to 2.6 ms at 150 MHz (GEN2)

Update the PR to get rid completely of random indexing copy as all privates are blinded already

Comment thread sw/device/lib/crypto/drivers/otbn.h Outdated
* Write to OTBN's data memory (DMEM).
*
* To mitigate SCA, write in random order to the DMEM.
* Writes data to OTBN's data memory (DMEM) and verifies the LOAD_CHECKSUM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we really should get rid of the random order here. It might be worth adding a new otbn_dmem_write_unardened function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. Updated the changes to add a new public data dmem copy

@GillonB
GillonB force-pushed the mldsa_verify_dmem_fast_opt branch from e2133e6 to d5d91ce Compare September 18, 2026 13:00

@siemen11 siemen11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @GillonB!! Amazing find and a great improvement for performance!

Comment thread sw/device/lib/crypto/drivers/otbn.c Outdated
crc32_add8(&ctx, (uint8_t)offset);
crc32_add8(&ctx, (uint8_t)(offset >> 8));

HARDENED_CHECK_LT(i, num_words);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one you can remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

OTBN_ADDR_T_INIT(mldsa87_verify, mldsa87_verify_pk);
HARDENED_TRY(otbn_dmem_write(public_key->key_length / sizeof(uint32_t),
public_key->key, kOtbnPk));
HARDENED_TRY(otbn_dmem_write_public(public_key->key_length / sizeof(uint32_t),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would then be consistent and apply it to all public data, I guess also RSA gains here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread sw/device/silicon_creator/lib/drivers/otbn.c
Add otbn_dmem_write_public() for linear
public transfers and optimize DMEM CRC-32
calculation using direct crc32_add32/add8
instructions.

Signed-off-by: Bastien Gillon <bgillon@google.com>
Add sc_otbn_dmem_write_public() in the silicon_creator
OTBN driver to perform linear MMIO stores for
non-sensitive public data without consuming PRNG
entropy, and add unit test coverage.

Signed-off-by: Bastien Gillon <bgillon@google.com>
…blic

Switch non-sensitive public parameters (public keys,
signatures, digests, ciphertext, modes) to
otbn_dmem_write_public() across RSA, ML-KEM, ML-DSA,
ECDSA, Ed25519, and ECDH.

Signed-off-by: Bastien Gillon <bgillon@google.com>
…mem_write_public

Switch non-sensitive public data transfers to
sc_otbn_dmem_write_public() for OTBN application
loading and boot services (sigverify, attestation,
and status clearing).

Signed-off-by: Bastien Gillon <bgillon@google.com>
@GillonB
GillonB force-pushed the mldsa_verify_dmem_fast_opt branch from d5d91ce to 9361c57 Compare September 19, 2026 13:29
const otbn_addr_t kOtbnVarRsaInOut = OTBN_ADDR_T_INIT(run_rsa, inout);
HARDENED_TRY(otbn_dmem_write(num_words, base, kOtbnVarRsaInOut));
const otbn_addr_t kOtbnVarRsaN = OTBN_ADDR_T_INIT(run_rsa, rsa_n);
HARDENED_TRY(otbn_dmem_write(num_words, modulus, kOtbnVarRsaN));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we write a public


// Set the base, the modulus n and private exponent d.
const otbn_addr_t kOtbnVarRsaInOut = OTBN_ADDR_T_INIT(run_rsa, inout);
HARDENED_TRY(otbn_dmem_write(num_words, base, kOtbnVarRsaInOut));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a public value (ciphertext or message digest)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants