nx_aes128_gcm.nx source
↩ module page · 281 lines · 9645 B
1// nx_aes128_gcm.nx -- canonical AES-128-GCM (AEAD) per NIST SP 800-38D
2// + RFC 5288 (TLS). 96-bit IV form only (the only form TLS 1.3 uses).
3//
4// COMPOSED, not re-implemented:
5// - AES-128 block encrypt + key expansion -> nx_aes.nx (canonical
6// S-box, MixColumns, ShiftRows, AddRoundKey)
7// - GF(2^128) multiplication / GHASH -> nx_ghash.nx
8//
9// Per the bit-level no-tool-proliferation cardinal, this file MUST
10// NOT re-implement either AES or GHASH inline. Future hardware-
11// accelerated variants (AES-NI on x86, AES instructions on aarch64)
12// ship as alternative backends behind the same API surface.
13//
14// Public API (TLS 1.3 record-layer compatible):
15// nx_aes128_gcm_seal(key16, iv12, aad, aad_len, pt, pt_len,
16// ct_out, tag16_out) -> 0
17// nx_aes128_gcm_open(key16, iv12, aad, aad_len, ct, ct_len, tag16,
18// pt_out) -> 0 | -1 (tag fail)
19//
20// Per Cardinals 9 (single-responsibility), 22 (composition).
21//
22// license_tier: INDEPENDENT_REDERIVE
23// genealogy_id: international-research-sources/nist/sp_800_38d + ietf/rfc_5288
24// lineage_id: nishi_aes128_gcm_q10
25
26// nx_safety_envelope:
27// intended_use: "TLS 1.3 AES-128-GCM cipher suite AEAD"
28// sil_target: SIL3
29// evidence: [composed_from_nx_aes_canonical, composed_from_nx_ghash,
30// NIST_SP_800_38D_test_vectors_VERIFIED]
31// verdict: NOT_YET_EVALUATED
32
33import "nx_syscalls.nx"
34import "nx_aes.nx"
35import "nx_ghash.nx"
36
37// Increment the rightmost 32 bits of a 16-byte counter block.
38// Per SP 800-38D ยง6.2 (inc32).
39func nx_aes128_gcm_inc32(ctr: *u8) -> i64 {
40 var i: i64 = 15
41 var done: i64 = 0
42 while done == 0 {
43 if i < 12 { done = 1 }
44 if done == 0 {
45 let nb: i64 = (ctr[i] + 1) & 0xff
46 ctr[i] = nb as u8
47 if nb != 0 { done = 1 }
48 else { i = i - 1 }
49 }
50 }
51 return 0
52}
53
54// Constant-time(ish) 16-byte buffer equality.
55func nx_aes128_gcm_tag_eq(a: *u8, b: *u8) -> i64 {
56 var diff: i64 = 0
57 var i: i64 = 0
58 while i < 16 {
59 diff = diff | ((a[i] ^ b[i]) & 0xff)
60 i = i + 1
61 }
62 if diff == 0 { return 1 }
63 return 0
64}
65
66// Encrypt one FULL CTR block (word ops, no per-byte loop): ks = E_K(ctr);
67// ct_out[pos..pos+16] = pt[pos..pos+16] XOR ks; ctr += 1 (inc32). Ciphertext is left in
68// ct_out for the GHASH pass to reflect. Caller guarantees pos + 16 <= pt_len.
69func gcm_ctr_store(ctr: *u8, sched: *u8, pt: *u8, ct_out: *u8, pos: i64, ks: *u8) -> i64 {
70 let ctw: *i64 = ctr as *i64
71 let ksw: *i64 = ks as *i64
72 ksw[0] = ctw[0]; ksw[1] = ctw[1]
73 let _e: i64 = __aes128_enc_block(ks, sched)
74 let pw: *i64 = ((pt as i64) + pos) as *i64
75 let ow: *i64 = ((ct_out as i64) + pos) as *i64
76 ow[0] = pw[0] ^ ksw[0]
77 ow[1] = pw[1] ^ ksw[1]
78 nx_aes128_gcm_inc32(ctr)
79 return 0
80}
81
82// AES-128-GCM seal (encrypt + authenticate).
83// Inputs:
84// key16: 16-byte AES key
85// iv12: 12-byte IV (TLS 1.3 supplies record nonce here)
86// aad: additional authenticated data (TLS record header for
87// TLS 1.3, length aad_len)
88// pt: plaintext bytes (length pt_len)
89// Outputs:
90// ct_out: pt_len ciphertext bytes (caller allocates)
91// tag16_out: 16-byte AEAD tag
92// Returns 0 on success.
93func nx_aes128_gcm_seal(key16: *u8, iv12: *u8,
94 aad: *u8, aad_len: i64,
95 pt: *u8, pt_len: i64,
96 ct_out: *u8, tag16_out: *u8) -> i64 {
97 let sched: *u8 = sys_mmap(176)
98 aes128_expand_key(key16, sched)
99
100 // H = E_K(0^128)
101 let zero: *u8 = sys_mmap(16)
102 var i: i64 = 0
103 while i < 16 { zero[i] = 0; i = i + 1 }
104 let h: *u8 = sys_mmap(16)
105 aes128_encrypt_block(zero, sched, h)
106
107 // J0 = iv12 || 0x00000001
108 let j0: *u8 = sys_mmap(16)
109 i = 0
110 while i < 12 { j0[i] = iv12[i]; i = i + 1 }
111 j0[12] = 0; j0[13] = 0; j0[14] = 0; j0[15] = 1
112
113 // E_K(J0) for tag mask
114 let ekj0: *u8 = sys_mmap(16)
115 aes128_encrypt_block(j0, sched, ekj0)
116
117 // FAST PATH: keep the GHASH accumulator in the reflected (PCLMULQDQ) domain so H is
118 // reflected ONCE here (rh) instead of every block, and the result reflected back ONCE
119 // at the end. py = phi(Y); phi(0)=0. Field-identical to the Horner path (gh_clmul_core
120 // under nx_ghash_clmul_gate).
121 let rh: *u8 = sys_mmap(16)
122 nx_ghash_reflect16(rh, h)
123 let py: *u8 = sys_mmap(16)
124 i = 0
125 while i < 16 { py[i] = 0; i = i + 1 }
126
127 // Reflected H-powers for 4-way aggregation: rh1..rh4 = phi(H^1..H^4). Computed once.
128 let rh2: *u8 = sys_mmap(16)
129 let rh3: *u8 = sys_mmap(16)
130 let rh4: *u8 = sys_mmap(16)
131 nx_ghash_mul_rev(rh, rh, rh2) // phi(H^2)
132 nx_ghash_mul_rev(rh2, rh, rh3) // phi(H^3)
133 nx_ghash_mul_rev(rh3, rh, rh4) // phi(H^4)
134
135 // AAD, zero-padded to 16-byte blocks (matches nx_ghash_update_buf semantics).
136 let ablk: *u8 = sys_mmap(16)
137 var apos: i64 = 0
138 while apos < aad_len {
139 var ab: i64 = 0
140 while ab < 16 {
141 if apos + ab < aad_len { ablk[ab] = aad[apos + ab] }
142 else { ablk[ab] = 0 }
143 ab = ab + 1
144 }
145 nx_ghash_upd_rev(py, rh, ablk)
146 apos = apos + 16
147 }
148
149 // CTR starts at J0+1. Encrypt + simultaneous GHASH of ciphertext.
150 let ctr: *u8 = sys_mmap(16)
151 i = 0
152 while i < 16 { ctr[i] = j0[i]; i = i + 1 }
153 nx_aes128_gcm_inc32(ctr)
154
155 let ks: *u8 = sys_mmap(16)
156 let ct_block: *u8 = sys_mmap(16)
157 let ksw: *i64 = ks as *i64
158 var pos: i64 = 0
159 // 4-way aggregated blocks: encrypt 4 CTR blocks (contiguous ciphertext into ct_out),
160 // then ONE aggregated GHASH update (single reduction over the group).
161 while pos + 64 <= pt_len {
162 gcm_ctr_store(ctr, sched, pt, ct_out, pos, ks)
163 gcm_ctr_store(ctr, sched, pt, ct_out, pos + 16, ks)
164 gcm_ctr_store(ctr, sched, pt, ct_out, pos + 32, ks)
165 gcm_ctr_store(ctr, sched, pt, ct_out, pos + 48, ks)
166 nx_ghash_upd_rev4(py, rh, rh2, rh3, rh4, ((ct_out as i64) + pos) as *u8)
167 pos = pos + 64
168 }
169 // leftover full 16-byte blocks (fewer than 4): single-block reflected update.
170 while pos + 16 <= pt_len {
171 gcm_ctr_store(ctr, sched, pt, ct_out, pos, ks)
172 nx_ghash_upd_rev(py, rh, ((ct_out as i64) + pos) as *u8)
173 pos = pos + 16
174 }
175 // ragged tail block (0 < pt_len - pos < 16): byte path, ciphertext zero-padded for GHASH.
176 if pos < pt_len {
177 let ctw2: *i64 = ctr as *i64
178 ksw[0] = ctw2[0]; ksw[1] = ctw2[1]
179 let _e2: i64 = __aes128_enc_block(ks, sched)
180 var b: i64 = 0
181 while b < 16 {
182 if pos + b >= pt_len { ct_block[b] = 0 }
183 else {
184 let c: i64 = (pt[pos + b] ^ ks[b]) & 0xff
185 ct_out[pos + b] = c as u8
186 ct_block[b] = c as u8
187 }
188 b = b + 1
189 }
190 nx_ghash_upd_rev(py, rh, ct_block)
191 nx_aes128_gcm_inc32(ctr)
192 pos = pos + 16
193 }
194
195 // Length block [aad_len_bits BE64][pt_len_bits BE64] + final GHASH.
196 let lenblk: *u8 = sys_mmap(16)
197 nx_ghash_be64_put(lenblk, aad_len * 8)
198 nx_ghash_be64_put(lenblk + 8, pt_len * 8)
199 nx_ghash_upd_rev(py, rh, lenblk)
200
201 // Y = phi(py); Tag = Y XOR E_K(J0).
202 let y: *u8 = sys_mmap(16)
203 nx_ghash_reflect16(y, py)
204 i = 0
205 while i < 16 {
206 tag16_out[i] = (y[i] ^ ekj0[i]) & 0xff
207 i = i + 1
208 }
209 return 0
210}
211
212// AES-128-GCM open (verify + decrypt).
213// Returns 0 on tag verify, -1 on tag mismatch. pt_out is only
214// populated on success.
215func nx_aes128_gcm_open(key16: *u8, iv12: *u8,
216 aad: *u8, aad_len: i64,
217 ct: *u8, ct_len: i64, tag16: *u8,
218 pt_out: *u8) -> i64 {
219 let sched: *u8 = sys_mmap(176)
220 aes128_expand_key(key16, sched)
221
222 // Same H + J0 + E_K(J0) as seal.
223 let zero: *u8 = sys_mmap(16)
224 var i: i64 = 0
225 while i < 16 { zero[i] = 0; i = i + 1 }
226 let h: *u8 = sys_mmap(16)
227 aes128_encrypt_block(zero, sched, h)
228
229 let j0: *u8 = sys_mmap(16)
230 i = 0
231 while i < 12 { j0[i] = iv12[i]; i = i + 1 }
232 j0[12] = 0; j0[13] = 0; j0[14] = 0; j0[15] = 1
233
234 let ekj0: *u8 = sys_mmap(16)
235 aes128_encrypt_block(j0, sched, ekj0)
236
237 // GHASH AAD || CT (note: opens authenticate over CIPHERTEXT,
238 // not plaintext, so the same Y is computed regardless of decrypt
239 // ordering).
240 let y: *u8 = sys_mmap(16)
241 i = 0
242 while i < 16 { y[i] = 0; i = i + 1 }
243 nx_ghash_update_buf(y, h, aad, aad_len)
244 nx_ghash_update_buf(y, h, ct, ct_len)
245 nx_ghash_finalize(y, h, aad_len, ct_len)
246
247 // Expected tag = Y XOR E_K(J0). Compare against supplied tag16.
248 let expected: *u8 = sys_mmap(16)
249 i = 0
250 while i < 16 {
251 expected[i] = (y[i] ^ ekj0[i]) & 0xff
252 i = i + 1
253 }
254 if nx_aes128_gcm_tag_eq(expected, tag16) != 1 { return 0 - 1 }
255
256 // Tag OK -- decrypt by re-running CTR mode.
257 let ctr: *u8 = sys_mmap(16)
258 i = 0
259 while i < 16 { ctr[i] = j0[i]; i = i + 1 }
260 nx_aes128_gcm_inc32(ctr)
261
262 let ks: *u8 = sys_mmap(16)
263 var pos: i64 = 0
264 while pos < ct_len {
265 aes128_encrypt_block(ctr, sched, ks)
266 var b: i64 = 0
267 while b < 16 {
268 if pos + b < ct_len {
269 pt_out[pos + b] = (ct[pos + b] ^ ks[b]) & 0xff
270 }
271 b = b + 1
272 }
273 nx_aes128_gcm_inc32(ctr)
274 pos = pos + 16
275 }
276 return 0
277}
278
279func main() -> i64 {
280 return 0
281}