nx_aes_ctr.nx source
↩ module page · 149 lines · 5938 B
1// nx_aes_ctr.nx -- AES-128 Counter (CTR) mode per NIST SP 800-38A §6.5.
2//
3// license_tier: INDEPENDENT_REDERIVE
4// genealogy_id: international-research-sources/nist/sp_800_38a
5//
6// Composes nx_aes (FIPS 197 AES-128 block cipher) with a counter to
7// produce a keystream, which is then XOR'd with plaintext to give
8// ciphertext. CTR is a SYMMETRIC mode: encrypt and decrypt are the
9// same operation (XOR is self-inverse). Foundation for AES-GCM
10// (which is CTR + GHASH).
11//
12// Counter format (this implementation):
13// bytes 0..11 : nonce / IV (caller-provided, must be unique per
14// message under the same key -- the "N" in NIST
15// "nonce")
16// bytes 12..15 : 32-bit big-endian counter starting at 1
17//
18// (RFC 3686 and TLS 1.3 use this same shape: 12-byte nonce + 4-byte
19// counter. SP 800-38A allows other splits; we choose the most-
20// deployed one.)
21//
22// Algorithm (SP 800-38A §6.5):
23// for each 16-byte block i of plaintext:
24// keystream_i = AES-Encrypt(counter_i, key)
25// ciphertext_i = plaintext_i XOR keystream_i
26// counter_i+1 = increment_32(counter_i)
27//
28// The final block may be partial: only len(plaintext_partial) bytes
29// of the keystream are used.
30//
31// SECURITY POSTURE:
32// CTR-only does NOT authenticate. An attacker that knows the
33// keystream (e.g. via plaintext recovery or nonce reuse) can flip
34// arbitrary ciphertext bits and the plaintext flips correspondingly
35// on decrypt. Production use must compose CTR with an authenticator
36// (HMAC, Poly1305, or full GCM with GHASH). Substrate's named
37// improvement: nx_aes_gcm.nx (queued).
38//
39// NONCE-REUSE WARNING:
40// Two messages encrypted with the same (key, nonce) leak the XOR
41// of the plaintexts (XOR cancels the shared keystream). Caller
42// MUST guarantee nonce uniqueness per key.
43//
44// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml)
45// intended_use: "AES-128-CTR mode -- TLS 1.3 record encryption /
46// bulk file encryption / IoT transport"
47// sil_target: SIL3 (cipher composition; nonce-misuse
48// is a confidentiality break)
49// asil_target: QM
50// dal_target: DAL B
51// iec_62304_class: B
52// evidence: [no_floating_point, sealed_enum_complete,
53// bit_equal_reproducible,
54// NIST_SP_800-38A_Sec_F.5_KAT_VERIFIED,
55// inherits_nx_aes_FIPS_197_evidence_chain]
56// hazard_register: [bug-tape-nonce-reuse-disclosure,
57// bug-tape-counter-wrap-after-2^64-blocks,
58// bug-tape-IV-derived-from-low-entropy-source]
59// residual_risk: "Nonce uniqueness is CALLER RESPONSIBILITY.
60// Substrate cannot enforce uniqueness without
61// external state. TLS 1.3 callers use the
62// record sequence number; offline callers
63// MUST source nonce from a CSPRNG and never
64// re-encrypt under the same (key, nonce).
65// The cache-timing risk inherited from nx_aes
66// applies; bitslice variant queued."
67// verdict: NOT_YET_EVALUATED
68
69import "nx_syscalls.nx"
70import "nx_aes.nx"
71
72const AES_CTR_NONCE_LEN: i64 = 12
73
74// Increment the 32-bit big-endian counter at bytes 12..15 of `block`.
75// Pure overflow semantics: 0xffffffff -> 0x00000000 (caller's job to
76// avoid; 2^32 blocks under one nonce is the SP 800-38A cap anyway).
77
78func _aes_ctr_inc(block: *u8) -> i64 {
79 var i: i64 = 15
80 while i >= 12 {
81 let b: i64 = (block[i] as i64) & 0xff
82 if b == 255 {
83 block[i] = 0
84 i = i - 1
85 } else {
86 block[i] = (b + 1) & 0xff
87 return 0
88 }
89 }
90 return 0
91}
92
93// ICB layout: 12-byte nonce + 32-bit big-endian counter (MSB first).
94// Caller may build inline OR via aes128_ctr_build_icb.
95//
96// History note: this helper exposed an emit_sd_sp register-collision
97// bug in riscv.c (t6 used as both value-to-store and address scratch
98// on large-offset spills). Fixed 2026-05-16 in riscv.c; helper now
99// works end-to-end with multi-block ctr_xor.
100
101func aes128_ctr_build_icb(icb: *u8, nonce: *u8, start_counter: i64) -> i64 {
102 var i: i64 = 0
103 while i < AES_CTR_NONCE_LEN { icb[i] = nonce[i]; i = i + 1 }
104 icb[15] = start_counter & 0xff
105 icb[14] = (start_counter / 256) & 0xff
106 icb[13] = (start_counter / 65536) & 0xff
107 icb[12] = (start_counter / 16777216) & 0xff
108 return 0
109}
110
111// Public: AES-128-CTR. Encrypt or decrypt (symmetric).
112// Caller pre-builds the 16-byte ICB (e.g. via aes128_ctr_build_icb).
113//
114// sched: 176-byte expanded key from aes128_expand_key
115// icb: 16-byte initial counter block (will be modified in place)
116// in_buf: plaintext (encrypt) or ciphertext (decrypt)
117// in_len: bytes
118// out_buf: result (may alias in_buf)
119
120func aes128_ctr_xor(sched: *u8, icb: *u8, in_buf: *u8, in_len: i64, out_buf: *u8) -> i64 {
121 if in_len <= 0 { return 0 }
122 let keystream: *u8 = sys_mmap(32)
123 var off: i64 = 0
124 var i: i64 = 0
125 var take: i64 = 0
126 while off < in_len {
127 aes128_encrypt_block(icb, sched, keystream)
128 let remaining: i64 = in_len - off
129 if remaining < AES_BLOCK {
130 take = remaining
131 } else {
132 take = AES_BLOCK
133 }
134 i = 0
135 while i < take {
136 let p_in: *u8 = (in_buf as i64 + off + i) as *u8
137 let p_out: *u8 = (out_buf as i64 + off + i) as *u8
138 let pb: i64 = (p_in[0] as i64) & 0xff
139 let kb: i64 = (keystream[i] as i64) & 0xff
140 p_out[0] = (pb ^ kb) & 0xff
141 i = i + 1
142 }
143 off = off + take
144 if off < in_len {
145 _aes_ctr_inc(icb)
146 }
147 }
148 return in_len
149}