nx_ct.nx
buildroot/runtime/nx_ct.nx
about
ct.nx -- constant-time arithmetic primitives.
Every crypto primitive -- hash, cipher, MAC, KEM, signature --
that touches secret data MUST route through these helpers. The
contract: the running time of each function is independent of
input values, only of input lengths. No branches on secret bits,
no indexed lookups driven by secret bits, no early exits.
Why: without constant-time execution, even a cryptographically
perfect algorithm leaks secrets through timing (Kocher 1996),
power analysis (DPA, Kocher 1999), and cache side-channels
(Spectre 2018, Flush+Reload 2014). These leaks defeat RSA, AES,
ECDSA, and every post-quantum scheme that NIST standardised.
Adversary model we harden against:
- Timing from shared clocks (local or remote)
- Hyperthread / SMT cache interference
- Power/EM on modest hardware
Out of scope (addressed at silicon phase, not in this file):
- Speculative execution (Spectre family)
- Rowhammer DRAM fault injection
- Physical glitching / laser fault injection
Research:
- "Cryptographic Engineering" (KoƧ 2008), ch. 11
- "The Boringssl/Fiat-Crypto constant-time subset" (Erbsen 2019)
- NIST SP 800-140A side-channel resistance requirements
Invariants enforced, not assumed:
C1 No branch depends on a byte or word value that could be
secret. Branches on *lengths* are allowed -- length is
assumed to be known to the adversary anyway.
C2 No indexed memory load uses a secret value as the index
(prevents cache-line-sampling attacks).
C3 No early return short-circuits a comparison; every byte
of the fixed-length input participates in the result.
C4 Arithmetic uses unsigned semantics where possible so the
optimiser never rewrites branchless code into branched
code (signed overflow is UB in C and a rewrite trigger;
NishiLang has no UB but we still prefer the unsigned
dependencies 0 imports · 7 importers
imports: none
imported by: nx_aead.nxnx_basic_auth.nxnx_csrf_token.nxnx_ed25519.nxnx_jwt.nxnx_signed_cookie.nxnx_totp_verify.nx
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| none |
functions
| 49 | func ct_eq(a: i64, b: i64) -> i64 { |
| 67 | func ct_lt(a: i64, b: i64) -> i64 {
called by 1: main |
| 86 | func ct_select(cond: i64, a: i64, b: i64) -> i64 {
called by 1: main |
| 98 | func ct_memcmp(a: *u8, b: *u8, n: i64) -> i64 {
called by 6: aead_openmainbasic_auth_verifycsrf_token_verifyjwt_verify_hs256signed_cookie_verify calls 1: ct_eq |
| 114 | func ct_copy_cond(cond: i64, dst: *u8, src: *u8, n: i64) -> i64 { |
| 130 | func ct_is_zero(buf: *u8, n: i64) -> i64 {
calls 1: ct_eq |
| 146 | func main() -> i64 { |