Shared side-channel primitives — silentops
The silentops crate is the single source of truth for the
low-level side-channel primitives used by arcana (and by
quantica on the post-quantum side). Keeping these primitives
in a separate crate means:
a single audit surface for CT correctness, independent of any particular algorithm;
architecture-specific assembly backends selected at compile time via Cargo features, so a downstream crate never embeds per-arch
asmin its own source;the same primitives are used by the statistical (
dudect) and the client-request (ctgrind) side-channel verifiers, keeping test coverage coherent.
This chapter is a reference for the primitives as used by
arcana. The silentops::ct API itself is documented in
quantica/doc/sca/primitives.rst (the two crates share the
identical surface); the additions below are the arcana-specific
notes about which primitives are wired into which arcana code
paths.
Constant-time selects
silentops::ct_select_u8 / ct_select_i16 / ct_select_i32
are the workhorse branchless selects. Within arcana they are
currently used by:
HMAC / CMAC / KMAC tag verification —
mac::ctx::Mac::verifydelegates tosilentops::ct_eqfor the tag comparison.AEAD tag verification —
cipher::modes::Gcm::decrypt,cipher::ccm::Ccm::decrypt,cipher::chacha20poly1305,cipher::xchacha20poly1305: same pattern,ct_eq.ECDSA verify — sig.r / sig.s structural checks (
ecc::ecdsa::verify_internal).
The ECC point-level CT-select used inside the Montgomery ladder is
arcana-local at present (ecc::curve::ct_select_point,
ecc::curve::ct_swap) because silentops::ct does not yet
expose a multi-limb / array select primitive. Migrating to
silentops once a ct_select_array family is available is
item T2-F in the roadmap.
Volatile zeroization
silentops::ct_zeroize and ct_zeroize_i16 are the canonical
volatile-write zeroizers (core::ptr::write_volatile +
compiler_fence(SeqCst)).
arcana does not yet use them in ``Drop`` for any of its typed
key wrappers. The README’s Typed key wrappers (Zeroize-on-Drop)
section flags this gap explicitly. Closing it is item T2-E: add
Drop impls calling silentops::ct_zeroize to:
rsa::rsa::RsaSecretKey— note that theBigIntfields (n,d,p,q,dp,dq,qinv) holdVec<u64>storage; theDropmust walk each one.ecc::eddsa::Ed25519SecretKey— fixed 32-byte array, trivial.ecc::curves::SecretKey—bytes: Vec<u8>.X25519 / X448: the API today consumes/produces raw byte arrays on the stack, so callers are responsible. A typed-wrapper layer is a candidate refresh once the ECC
SecretKeylands.
Compiler shielding (black_box) for bit-mask CT
LLVM at opt-level=2 and above has been observed (rustc 1.84+)
to recover a secret-dependent branch from the bit-mask select
pattern (x & mask) | (y & !mask) when mask is derived from
a cond ∈ {0, 1} value. This affects every classical CT field
operation that conditionally subtracts p after a sum or
comparison: field_add, field_sub, reduce_wide,
reduce_mod_n.
The arcana fix is to wrap the derived mask in
core::hint::black_box so the optimizer cannot trace its
provenance:
let need_sub = carry | (1u64.wrapping_sub(borrow));
let mask = core::hint::black_box(0u64.wrapping_sub(need_sub));
let inv_mask = !mask;
for j in 0..LIMBS {
remainder.limbs[j] =
(trial[j] & mask) | (remainder.limbs[j] & inv_mask);
}
This pattern lives at three sites in ecc::field: field_add,
field_sub, reduce_wide. It is not yet applied to
rsa::bigint — item T1-E includes that audit.
Verified release-asm branch counts (x86_64, all 4 curve widths)
post-commit 76191c1:
scalar_mul_point: 1 (loop counter)point_double: 0point_add_ct: inlined, 0 point-value branches
Compared to pre-commit 76191c1 (which had 8 branches in
point_double and 12 in point_add), this is a net source-
level CT win even before the silentops asm migration of
T2-F.
Statistical timing verification — silentops::verify
silentops::verify implements the dudect methodology
[RBV17]. Surface used by arcana today: none;
target use is documented in Verification methodology.
Planned arcana-side dudect harnesses (item T3-B):
scalar_mul_pointon each of the 7 short-Weierstrass curves — fixed-vs-random scalar.rsa_decrypt_raw(CRT path) — fixed-vs-random ciphertext.AEAD decrypt tag-check — fixed-vs-random tag (regression test on the
ct_eqchain).Once
T1-Alands: fixsliced AES round function — fixed-vs- random key.
Architecture dispatch
The silentops/src/ct/mod.rs file selects exactly one backend
at compile time. The table is identical between arcana and
quantica:
Target |
Cargo feature |
Method |
|---|---|---|
x86_64 |
|
Inline |
aarch64 |
|
Inline |
thumbv7em |
|
IT blocks + conditional execution |
thumbv6m |
|
AND/OR/XOR (no IT, no csel) |
riscv32 |
|
AND/OR/XOR (no cmov) |
default |
none |
Pure-Rust bit-mask fallback (relies on compiler + black_box) |
For arcana, the black_box shielding documented above is the
default-build CT guarantee. With an asm backend enabled, the
guarantee shifts to “the compiler cannot rewrite the asm” — a
strictly stronger property.