nx_u384.nx source
↩ module page · 241 lines · 8197 B
1// nx_u384.nx -- 384-bit unsigned big-int big-int (12 × 32-bit limbs).
2//
3// Mirrors nx_u256.nx structure exactly with limb count 12 instead
4// of 8. Needed for ECDSA P-384 (task #67) -- the curve has 384-bit
5// field elements and scalars. Real public chains (example.com etc.)
6// use P-384 keys on intermediate + root certs.
7//
8// Layout: 12 i64 slots, each holding one 32-bit limb in low bits.
9// Limb 0 is least-significant; limb 11 is most-significant.
10// Byte serialization: u384_load_be / u384_store_be use 48-byte
11// big-endian (DER) layout: bytes[0] is MSB, bytes[47] is LSB.
12//
13// API (mirrors u256 1:1):
14// u384_alloc() -> *i64 (12-limb zeroed buffer)
15// u384_zero(out)
16// u384_one(out)
17// u384_copy(out, src)
18// u384_load_be(out, bytes) bytes is 48 BE octets
19// u384_store_be(bytes, src)
20// u384_add_with_carry(out, a, b) -> final carry-out 0|1
21// u384_sub_with_borrow(out, a, b) -> final borrow-out 0|1
22// u384_cmp(a, b) -> -1 | 0 | +1
23// u384_is_zero(a) -> 0 | 1
24// u384_eq(a, b) -> 0 | 1
25//
26// license_tier: INDEPENDENT_REDERIVE
27// genealogy_id: international-research-sources/nist/sec1_v2_appendix_b + nist/fips_186_5
28// lineage_id: nishi_u384_q10
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-20, p384-bigint-foundation]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37
38const NX_U384_LIMBS: i64 = 12
39const NX_U384_BYTES: i64 = 48
40const NX_U384_LIMB_BITS: i64 = 32
41const NX_U384_LIMB_MASK: i64 = 0xFFFFFFFF
42
43// --- u384 scratch arena (bump allocator) -- mirrors nx_u256's NX_SCRATCH (proven). ----------------------
44// THE certloop mmap-leak fix: the hot P-384 paths (point double/add, scalar-mul) allocate field scratch per
45// call; one ECDSA-P384 verify cumulatively touched ~16k tiny mmaps and never freed them, so a TLS cert chain
46// (3 ECDSA links) exhausted the process's mmap regions after ~15 fetches. Now u384_alloc bump-allocates from a
47// reused arena and the verify/scalar-mul wrap themselves in nx_u384_scratch_save/restore (LIFO O(1) reclaim) ->
48// every transient is reclaimed per verify, the arena peak stays a couple MiB and is reused, zero VMA growth.
49const NX_U384_SCRATCH_BLOCK: i64 = 1048576 // 1 MiB per block
50const NX_U384_SCRATCH_MAXBLOCKS: i64 = 8192 // 8 GiB ceiling (pathological-leak backstop only)
51static NX_U384_SCRATCH_BLOCKS: i64 // addr of i64[MAXBLOCKS] (block base addrs); 0 = uninit
52static NX_U384_SCRATCH_NBLOCKS: i64 // blocks mmap'd so far
53static NX_U384_SCRATCH_CUR: i64 // virtual cursor (bytes)
54
55func nx_u384_scratch_init() -> i64 {
56 if NX_U384_SCRATCH_BLOCKS == 0 {
57 let arr: *u8 = sys_mmap(NX_U384_SCRATCH_MAXBLOCKS * 8)
58 NX_U384_SCRATCH_BLOCKS = arr as i64
59 let blocks: *i64 = NX_U384_SCRATCH_BLOCKS as *i64
60 let b0: *u8 = sys_mmap(NX_U384_SCRATCH_BLOCK)
61 blocks[0] = b0 as i64
62 NX_U384_SCRATCH_NBLOCKS = 1
63 NX_U384_SCRATCH_CUR = 0
64 }
65 return 0
66}
67
68func nx_u384_scratch(n: i64) -> *u8 {
69 nx_u384_scratch_init()
70 let aligned: i64 = (n + 7) / 8 * 8
71 if aligned > NX_U384_SCRATCH_BLOCK {
72 sys_write(2, "FATAL: u384 scratch alloc exceeds block size\n" as *u8, 45)
73 sys_exit(70)
74 }
75 var bi: i64 = NX_U384_SCRATCH_CUR / NX_U384_SCRATCH_BLOCK
76 var off: i64 = NX_U384_SCRATCH_CUR - bi * NX_U384_SCRATCH_BLOCK
77 if off + aligned > NX_U384_SCRATCH_BLOCK {
78 bi = bi + 1
79 off = 0
80 NX_U384_SCRATCH_CUR = bi * NX_U384_SCRATCH_BLOCK
81 }
82 let blocks: *i64 = NX_U384_SCRATCH_BLOCKS as *i64
83 while NX_U384_SCRATCH_NBLOCKS <= bi {
84 if NX_U384_SCRATCH_NBLOCKS >= NX_U384_SCRATCH_MAXBLOCKS {
85 sys_write(2, "FATAL: u384 scratch arena hit block ceiling (leak?)\n" as *u8, 52)
86 sys_exit(71)
87 }
88 let nb: *u8 = sys_mmap(NX_U384_SCRATCH_BLOCK)
89 blocks[NX_U384_SCRATCH_NBLOCKS] = nb as i64
90 NX_U384_SCRATCH_NBLOCKS = NX_U384_SCRATCH_NBLOCKS + 1
91 }
92 let base: i64 = blocks[bi]
93 let p: *u8 = (base + off) as *u8
94 NX_U384_SCRATCH_CUR = bi * NX_U384_SCRATCH_BLOCK + off + aligned
95 return p
96}
97
98// LIFO frame: save returns the cursor; restore rewinds, reclaiming every alloc since (blocks stay + reused).
99func nx_u384_scratch_save() -> i64 { nx_u384_scratch_init(); return NX_U384_SCRATCH_CUR }
100func nx_u384_scratch_restore(mark: i64) -> i64 { NX_U384_SCRATCH_CUR = mark; return 0 }
101
102func u384_alloc() -> *i64 {
103 let p: *i64 = nx_u384_scratch(NX_U384_LIMBS * 8) as *i64
104 var i: i64 = 0
105 while i < NX_U384_LIMBS {
106 p[i] = 0
107 i = i + 1
108 }
109 return p
110}
111
112// Arena-backed: freeing is the caller's save/restore frame (O(1) bulk reclaim). No-op kept for API compat so the
113// hundreds of existing u384_free call sites stay valid; real reclamation is the per-verify nx_u384_scratch_restore.
114func u384_free(p: *i64) -> i64 {
115 return 0
116}
117
118func u384_zero(out: *i64) -> i64 {
119 var i: i64 = 0
120 while i < NX_U384_LIMBS {
121 out[i] = 0
122 i = i + 1
123 }
124 return 0
125}
126
127func u384_one(out: *i64) -> i64 {
128 u384_zero(out)
129 out[0] = 1
130 return 0
131}
132
133func u384_copy(out: *i64, src: *i64) -> i64 {
134 var i: i64 = 0
135 while i < NX_U384_LIMBS {
136 out[i] = src[i]
137 i = i + 1
138 }
139 return 0
140}
141
142// Load 48 BE bytes into the 12-limb little-endian limb layout.
143// bytes[i*4..i*4+3] map BE to limb[11-i] for i in 0..12. Inside
144// each 4-byte group: bytes[off] is the high byte of the limb,
145// bytes[off+3] is the low byte.
146func u384_load_be(out: *i64, bytes: *u8) -> i64 {
147 var i: i64 = 0
148 while i < NX_U384_LIMBS {
149 let off: i64 = i * 4
150 let limb_idx: i64 = NX_U384_LIMBS - 1 - i
151 let b0: i64 = bytes[off] & 0xff
152 let b1: i64 = bytes[off + 1] & 0xff
153 let b2: i64 = bytes[off + 2] & 0xff
154 let b3: i64 = bytes[off + 3] & 0xff
155 out[limb_idx] = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
156 i = i + 1
157 }
158 return 0
159}
160
161func u384_store_be(bytes: *u8, src: *i64) -> i64 {
162 var i: i64 = 0
163 while i < NX_U384_LIMBS {
164 let off: i64 = i * 4
165 let limb_idx: i64 = NX_U384_LIMBS - 1 - i
166 let limb: i64 = src[limb_idx]
167 bytes[off] = ((limb >> 24) & 0xff) as u8
168 bytes[off + 1] = ((limb >> 16) & 0xff) as u8
169 bytes[off + 2] = ((limb >> 8) & 0xff) as u8
170 bytes[off + 3] = (limb & 0xff) as u8
171 i = i + 1
172 }
173 return 0
174}
175
176func u384_add_with_carry(out: *i64, a: *i64, b: *i64) -> i64 {
177 var i: i64 = 0
178 var carry: i64 = 0
179 while i < NX_U384_LIMBS {
180 let s: i64 = (a[i] & NX_U384_LIMB_MASK) + (b[i] & NX_U384_LIMB_MASK) + carry
181 out[i] = s & NX_U384_LIMB_MASK
182 carry = (s >> NX_U384_LIMB_BITS) & 1
183 i = i + 1
184 }
185 return carry
186}
187
188func u384_sub_with_borrow(out: *i64, a: *i64, b: *i64) -> i64 {
189 var i: i64 = 0
190 var borrow: i64 = 0
191 while i < NX_U384_LIMBS {
192 let d: i64 = (a[i] & NX_U384_LIMB_MASK) - (b[i] & NX_U384_LIMB_MASK) - borrow
193 if d < 0 {
194 out[i] = (d + (1 << NX_U384_LIMB_BITS)) & NX_U384_LIMB_MASK
195 borrow = 1
196 } else {
197 out[i] = d & NX_U384_LIMB_MASK
198 borrow = 0
199 }
200 i = i + 1
201 }
202 return borrow
203}
204
205func u384_cmp(a: *i64, b: *i64) -> i64 {
206 var i: i64 = NX_U384_LIMBS - 1
207 while i >= 0 {
208 let av: i64 = a[i] & NX_U384_LIMB_MASK
209 let bv: i64 = b[i] & NX_U384_LIMB_MASK
210 if av < bv { return 0 - 1 }
211 if av > bv { return 1 }
212 i = i - 1
213 }
214 return 0
215}
216
217func u384_is_zero(a: *i64) -> i64 {
218 var i: i64 = 0
219 var acc: i64 = 0
220 while i < NX_U384_LIMBS {
221 acc = acc | (a[i] & NX_U384_LIMB_MASK)
222 i = i + 1
223 }
224 if acc == 0 { return 1 }
225 return 0
226}
227
228func u384_eq(a: *i64, b: *i64) -> i64 {
229 var i: i64 = 0
230 var diff: i64 = 0
231 while i < NX_U384_LIMBS {
232 diff = diff | ((a[i] ^ b[i]) & NX_U384_LIMB_MASK)
233 i = i + 1
234 }
235 if diff == 0 { return 1 }
236 return 0
237}
238
239func main() -> i64 {
240 return 0
241}