nx_cert_gen.nx source
↩ module page · 398 lines · 16325 B
1// nx_cert_gen.nx -- V-HOST-4c: sovereign X.509 self-signed cert CLI driver.
2//
3// Closes the openssl bridge from NISHI_ANDELINWEST_DEPLOY_PLAYBOOK ยง3.
4// Full sovereign Zone S cert generation per License Containment Cardinal.
5//
6// USAGE (V1 hardcoded; V-PROV-1 CLI argv parser extends):
7// nx_cert_gen.elf \
8// --domain andelinwest.com \
9// --san www.andelinwest.com,wiki.andelinwest.com \
10// --validity-days 365 \
11// --out-dir /var/nishi-host/certs/andelinwest/
12//
13// Produces:
14// /var/nishi-host/certs/andelinwest/cert.der (X.509 v3 self-signed)
15// /var/nishi-host/certs/andelinwest/priv.ed25519 (32-byte raw priv)
16// /var/nishi-host/certs/andelinwest/pub.ed25519 (32-byte raw pub; for verify)
17//
18// COMPOSES (per "avoid duplicate primitives"):
19// hub/nx_x509_build (V-HOST-4b; X.509 v3 builder)
20// hub/nx_asn1_write (V-HOST-4a; DER encoder; transitively)
21// nx_csprng (sovereign CSPRNG; sys_getrandom OR /dev/urandom)
22// nx_ed25519_signature (ed25519_pub_from_priv just added)
23// nx_syscalls (sys_now_ms; sys_openat_wr; sys_write; sys_close)
24//
25// V-HOST-4c SCOPE:
26// - Generate 32-byte Ed25519 priv via nx_csprng_fill
27// - Derive 32-byte pub via ed25519_pub_from_priv
28// - Compute notBefore (now Unix sec -> GeneralizedTime) +
29// notAfter (now + validity_days)
30// - Build cert DER via nx_x509_build_self_signed
31// - Write cert.der + priv.ed25519 + pub.ed25519 to disk
32//
33// V-HOST-4c NON-SCOPE:
34// - Full argv parsing (V-PROV-1 CLI; V-HOST-4c uses hardcoded for V1 demo)
35// - PEM emit (only DER; convert via existing nx_pem_encode if shipped)
36// - chmod 0600 on priv (operator does this post-creation; or sys_chmod V+1)
37//
38// Status: V-HOST-4c. 2026-05-27.
39
40import "nx_syscalls.nx"
41import "hub/nx_x509_build.nx"
42import "hub/nx_asn1_write.nx"
43import "nx_csprng.nx"
44import "nx_ed25519_signature.nx"
45// V-HOST-4d-2: ECDSA P-256 path additions
46import "nx_u256.nx"
47import "nx_p256_point.nx"
48import "nx_p256_scalar_mul.nx"
49
50// ===== Sealed verdict surface (codes 3640-3654) =================================================
51const NX_CG_OK: i64 = 0
52const NX_CG_BAD_INPUT: i64 = 3640
53const NX_CG_CSPRNG_FAILED: i64 = 3641
54const NX_CG_KEYGEN_FAILED: i64 = 3642
55const NX_CG_TIME_FAILED: i64 = 3643
56const NX_CG_CERT_BUILD_FAILED: i64 = 3644
57const NX_CG_FILE_WRITE_FAILED: i64 = 3645
58
59// ===== Named constants (M7) =================================================
60const NX_CG_CERT_DER_CAP: i64 = 8192
61const NX_CG_FILE_OPEN_MODE_PRIV: i64 = 384 // 0o600 (rw-------)
62const NX_CG_FILE_OPEN_MODE_PUB: i64 = 420 // 0o644 (rw-r--r--)
63const NX_CG_DEFAULT_VALIDITY_DAYS: i64 = 365
64
65// ===== GeneralizedTime formatting from Unix seconds =================================================
66//
67// Format: YYYYMMDDhhmmssZ (15 bytes).
68// Computes from Unix-seconds-since-epoch using sealed leap-year-aware
69// Civil/Day calculation (per Howard Hinnant's date algorithms; public
70// domain math; substrate-original implementation).
71
72func nx_cg_days_from_civil(y: i64, m: i64, d: i64) -> i64 {
73 var year: i64 = y
74 if m <= 2 { year = year - 1 }
75 let era: i64 = if year >= 0 then year / 400 else (year - 399) / 400
76 let yoe: i64 = year - era * 400
77 var moy_factor: i64 = m + 9
78 if m > 2 { moy_factor = m - 3 }
79 let doy: i64 = (153 * moy_factor + 2) / 5 + d - 1
80 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy
81 return era * 146097 + doe - 719468
82}
83
84func nx_cg_civil_from_days(days: i64,
85 out_y: *i64, out_m: *i64, out_d: *i64) -> i64 {
86 let z: i64 = days + 719468
87 let era: i64 = if z >= 0 then z / 146097 else (z - 146096) / 146097
88 let doe: i64 = z - era * 146097
89 let yoe: i64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
90 let y: i64 = yoe + era * 400
91 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
92 let mp: i64 = (5 * doy + 2) / 153
93 let d: i64 = doy - (153 * mp + 2) / 5 + 1
94 var m: i64 = mp + 3
95 if mp >= 10 { m = mp - 9 }
96 var yy: i64 = y
97 if m <= 2 { yy = y + 1 }
98 out_y[0] = yy
99 out_m[0] = m
100 out_d[0] = d
101 return 0
102}
103
104// Format unix_sec into YYYYMMDDhhmmssZ (15 bytes).
105// Caller-allocated out buffer (>= 15 bytes).
106func nx_cg_format_generalized_time(unix_sec: i64, out_15: *u8) -> i64 {
107 if (out_15 as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
108 if unix_sec < 0 { return 0 - NX_CG_TIME_FAILED }
109 let days: i64 = unix_sec / 86400
110 let sec_of_day: i64 = unix_sec - days * 86400
111 let hh: i64 = sec_of_day / 3600
112 let mm: i64 = (sec_of_day - hh * 3600) / 60
113 let ss: i64 = sec_of_day - hh * 3600 - mm * 60
114
115 let y_p: *i64 = (sys_mmap(8)) as *i64
116 let m_p: *i64 = (sys_mmap(8)) as *i64
117 let d_p: *i64 = (sys_mmap(8)) as *i64
118 nx_cg_civil_from_days(days, y_p, m_p, d_p)
119
120 let y: i64 = y_p[0]
121 let mo: i64 = m_p[0]
122 let d: i64 = d_p[0]
123
124 // Pad: YYYY (4) MM (2) DD (2) hh (2) mm (2) ss (2) Z (1) = 15
125 out_15[0] = ((0x30 + ((y / 1000) % 10)) & 0xff) as u8
126 out_15[1] = ((0x30 + ((y / 100) % 10)) & 0xff) as u8
127 out_15[2] = ((0x30 + ((y / 10) % 10)) & 0xff) as u8
128 out_15[3] = ((0x30 + ( y % 10)) & 0xff) as u8
129 out_15[4] = ((0x30 + (mo / 10)) & 0xff) as u8
130 out_15[5] = ((0x30 + (mo % 10)) & 0xff) as u8
131 out_15[6] = ((0x30 + (d / 10)) & 0xff) as u8
132 out_15[7] = ((0x30 + (d % 10)) & 0xff) as u8
133 out_15[8] = ((0x30 + (hh / 10)) & 0xff) as u8
134 out_15[9] = ((0x30 + (hh % 10)) & 0xff) as u8
135 out_15[10] = ((0x30 + (mm / 10)) & 0xff) as u8
136 out_15[11] = ((0x30 + (mm % 10)) & 0xff) as u8
137 out_15[12] = ((0x30 + (ss / 10)) & 0xff) as u8
138 out_15[13] = ((0x30 + (ss % 10)) & 0xff) as u8
139 out_15[14] = 0x5A as u8 // 'Z'
140 return NX_CG_OK
141}
142
143// ===== Write file with sealed mode (composes existing sys_openat_wr + sys_write) =================================================
144
145func nx_cg_write_file(path_z: *u8, mode: i64,
146 data: *u8, data_n: i64) -> i64 {
147 if (path_z as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
148 let fd: i64 = sys_openat_wr(path_z, mode)
149 if fd < 0 { return 0 - NX_CG_FILE_WRITE_FAILED }
150 let written: i64 = sys_write(fd, data, data_n)
151 sys_close(fd)
152 if written != data_n { return 0 - NX_CG_FILE_WRITE_FAILED }
153 return NX_CG_OK
154}
155
156// ===== Top-level: generate self-signed cert for one domain =================================================
157//
158// Inputs: domain + SAN list + validity-days + output-dir (all paths;
159// V1 hardcoded SAN with up to 4 entries from caller-supplied parallel
160// arrays; V-PROV-1 CLI argv parser extends to ~16).
161
162func nx_cert_gen_self_signed(
163 domain: *u8, domain_n: i64,
164 san_ptrs: *i64, san_lens: *i64, san_count: i64,
165 validity_days: i64,
166 out_cert_path: *u8, out_priv_path: *u8, out_pub_path: *u8
167) -> i64 {
168 if (domain as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
169 if domain_n < 1 { return 0 - NX_CG_BAD_INPUT }
170 if validity_days < 1 { return 0 - NX_CG_BAD_INPUT }
171 if validity_days > 36500 { return 0 - NX_CG_BAD_INPUT } // ~100 year max
172
173 // 1. Generate 32-byte Ed25519 priv via sovereign CSPRNG
174 let priv_32: *u8 = sys_mmap(32)
175 let csprng_rc: i64 = nx_csprng_fill(priv_32, 32)
176 if csprng_rc != 0 { return 0 - NX_CG_CSPRNG_FAILED }
177
178 // 2. Derive 32-byte pub
179 let pub_32: *u8 = sys_mmap(32)
180 let keygen_rc: i64 = ed25519_pub_from_priv(priv_32, pub_32)
181 if keygen_rc != 0 { return 0 - NX_CG_KEYGEN_FAILED }
182
183 // 3. Compute notBefore + notAfter -- WALL CLOCK (CLOCK_REALTIME).
184 // Monotonic time is uptime, which encodes to ~1970 and makes the
185 // cert already-expired; X.509 validity must be calendar time.
186 let now_sec: i64 = sys_now_realtime_sec()
187 let not_before_sec: i64 = now_sec
188 let not_after_sec: i64 = now_sec + validity_days * 86400
189
190 let not_before: *u8 = sys_mmap(16)
191 let not_after: *u8 = sys_mmap(16)
192 let tb_rc: i64 = nx_cg_format_generalized_time(not_before_sec, not_before)
193 if tb_rc != NX_CG_OK { return tb_rc }
194 let ta_rc: i64 = nx_cg_format_generalized_time(not_after_sec, not_after)
195 if ta_rc != NX_CG_OK { return ta_rc }
196
197 // 4. Build cert via nx_x509_build_self_signed
198 let inp: *NxX509BuildInputs = (sys_mmap(128)) as *NxX509BuildInputs
199 let serial: i64 = now_sec // V1: time-based serial; V+1: random 16 bytes
200 let in_rc: i64 = nx_x509_inputs_init(inp, domain, domain_n,
201 san_ptrs, san_lens, san_count,
202 priv_32, pub_32,
203 not_before, not_after,
204 serial)
205 if in_rc != NX_X509_OK { return 0 - NX_CG_CERT_BUILD_FAILED }
206
207 let cert_der: *u8 = sys_mmap(NX_CG_CERT_DER_CAP)
208 let cert_der_n: *i64 = (sys_mmap(8)) as *i64
209 cert_der_n[0] = 0
210 let build_rc: i64 = nx_x509_build_self_signed(inp, cert_der, NX_CG_CERT_DER_CAP, cert_der_n)
211 if build_rc != NX_X509_OK { return 0 - NX_CG_CERT_BUILD_FAILED }
212
213 // 5. Write outputs
214 let cert_rc: i64 = nx_cg_write_file(out_cert_path, NX_CG_FILE_OPEN_MODE_PUB,
215 cert_der, cert_der_n[0])
216 if cert_rc != NX_CG_OK { return cert_rc }
217
218 let priv_rc: i64 = nx_cg_write_file(out_priv_path, NX_CG_FILE_OPEN_MODE_PRIV,
219 priv_32, 32)
220 if priv_rc != NX_CG_OK { return priv_rc }
221
222 let pub_rc: i64 = nx_cg_write_file(out_pub_path, NX_CG_FILE_OPEN_MODE_PUB,
223 pub_32, 32)
224 if pub_rc != NX_CG_OK { return pub_rc }
225
226 return NX_CG_OK
227}
228
229// ===== V-HOST-4d-2: ECDSA P-256 keypair generation =================================================
230//
231// 1. priv_bytes = csprng_fill(32)
232// 2. priv_limbs = u256_load_be(priv_bytes)
233// (no reduction; nx_ecdsa_p256_sign rejects priv >= n; CSPRNG output
234// has astronomically low collision probability with n upper bytes;
235// V+1 adds mod-n reduction for strict correctness)
236// 3. G = p256_point_load_g()
237// 4. pub_point = priv_limbs * G
238// 5. pub_point -> affine -> (pub_x_limbs, pub_y_limbs)
239//
240// Caller-allocated outputs (priv_limbs, pub_x, pub_y all *i64 4-limb).
241
242func nx_cg_ecdsa_p256_keygen(priv_limbs: *i64,
243 pub_x: *i64, pub_y: *i64) -> i64 {
244 if (priv_limbs as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
245 if (pub_x as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
246 if (pub_y as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
247
248 // 1. Random 32 bytes
249 let priv_bytes: *u8 = sys_mmap(32)
250 let csprng_rc: i64 = nx_csprng_fill(priv_bytes, 32)
251 if csprng_rc != 0 { return 0 - NX_CG_CSPRNG_FAILED }
252
253 // 2. Load into u256 limbs
254 u256_load_be(priv_limbs, priv_bytes)
255
256 // 3. Load generator G
257 let G: *P256Point = p256_point_alloc()
258 p256_point_load_g(G)
259
260 // 4. pub = priv * G
261 let pub_point: *P256Point = p256_point_alloc()
262 let mul_rc: i64 = p256_scalar_mul(pub_point, priv_limbs, G)
263 if mul_rc != NX_ECDSA_OK { return 0 - NX_CG_KEYGEN_FAILED }
264
265 // 5. Convert to affine
266 let aff_rc: i64 = p256_point_to_affine(pub_point)
267 if aff_rc != NX_P256_FIELD_OK { return 0 - NX_CG_KEYGEN_FAILED }
268
269 // 6. Copy x, y to caller-supplied limb arrays
270 u256_copy(pub_x, pub_point.x)
271 u256_copy(pub_y, pub_point.y)
272
273 return NX_CG_OK
274}
275
276// ===== V-HOST-4d-2: ECDSA P-256 self-signed cert generation =================================================
277//
278// Parallel to nx_cert_gen_self_signed; dispatches to ECDSA path.
279
280func nx_cert_gen_self_signed_ecdsa_p256(
281 domain: *u8, domain_n: i64,
282 san_ptrs: *i64, san_lens: *i64, san_count: i64,
283 validity_days: i64,
284 out_cert_path: *u8, out_priv_path: *u8, out_pub_path: *u8
285) -> i64 {
286 if (domain as i64) == 0 { return 0 - NX_CG_BAD_INPUT }
287 if domain_n < 1 { return 0 - NX_CG_BAD_INPUT }
288 if validity_days < 1 { return 0 - NX_CG_BAD_INPUT }
289 if validity_days > 36500 { return 0 - NX_CG_BAD_INPUT }
290
291 // 1. Generate ECDSA P-256 keypair
292 let priv_limbs: *i64 = u256_alloc()
293 let pub_x_limbs: *i64 = u256_alloc()
294 let pub_y_limbs: *i64 = u256_alloc()
295 let keygen_rc: i64 = nx_cg_ecdsa_p256_keygen(priv_limbs, pub_x_limbs, pub_y_limbs)
296 if keygen_rc != NX_CG_OK { return keygen_rc }
297
298 // 2. Time -- wall clock (CLOCK_REALTIME), not monotonic uptime.
299 let now_sec: i64 = sys_now_realtime_sec()
300 let not_before: *u8 = sys_mmap(16)
301 let not_after: *u8 = sys_mmap(16)
302 nx_cg_format_generalized_time(now_sec, not_before)
303 nx_cg_format_generalized_time(now_sec + validity_days * 86400, not_after)
304
305 // 3. Build cert via ECDSA inputs init
306 let inp: *NxX509BuildInputs = (sys_mmap(192)) as *NxX509BuildInputs
307 let serial: i64 = now_sec
308 let in_rc: i64 = nx_x509_inputs_init_ecdsa_p256(
309 inp, domain, domain_n,
310 san_ptrs, san_lens, san_count,
311 priv_limbs, pub_x_limbs, pub_y_limbs,
312 not_before, not_after,
313 serial)
314 if in_rc != NX_X509_OK { return 0 - NX_CG_CERT_BUILD_FAILED }
315
316 let cert_der: *u8 = sys_mmap(NX_CG_CERT_DER_CAP)
317 let cert_der_n: *i64 = (sys_mmap(8)) as *i64
318 cert_der_n[0] = 0
319 let build_rc: i64 = nx_x509_build_self_signed(inp, cert_der, NX_CG_CERT_DER_CAP, cert_der_n)
320 if build_rc != NX_X509_OK { return 0 - NX_CG_CERT_BUILD_FAILED }
321
322 // 4. Write cert.der (operator-readable)
323 let cert_rc: i64 = nx_cg_write_file(out_cert_path, NX_CG_FILE_OPEN_MODE_PUB,
324 cert_der, cert_der_n[0])
325 if cert_rc != NX_CG_OK { return cert_rc }
326
327 // 5. Write priv as big-endian 32 bytes (operator-private; mode 0600)
328 let priv_bytes: *u8 = sys_mmap(32)
329 u256_store_be(priv_bytes, priv_limbs)
330 let priv_rc: i64 = nx_cg_write_file(out_priv_path, NX_CG_FILE_OPEN_MODE_PRIV,
331 priv_bytes, 32)
332 if priv_rc != NX_CG_OK { return priv_rc }
333
334 // 6. Write pub as uncompressed point (0x04 || X || Y) -- 65 bytes
335 let pub_bytes: *u8 = sys_mmap(65)
336 pub_bytes[0] = 0x04 as u8
337 u256_store_be(((pub_bytes as i64) + 1) as *u8, pub_x_limbs)
338 u256_store_be(((pub_bytes as i64) + 33) as *u8, pub_y_limbs)
339 let pub_rc: i64 = nx_cg_write_file(out_pub_path, NX_CG_FILE_OPEN_MODE_PUB,
340 pub_bytes, 65)
341 if pub_rc != NX_CG_OK { return pub_rc }
342
343 return NX_CG_OK
344}
345
346// ===== V1 main: hardcoded andelinwest.com for demo =================================================
347//
348// V-PROV-1 CLI ships proper argv parsing; this main is the V-HOST-4c
349// proof-of-concept entry point.
350
351func main() -> i64 {
352 let domain: *u8 = "andelinwest.com" as *u8
353 let domain_n: i64 = 15
354
355 // SAN list: 2 entries
356 let san_ptrs: *i64 = (sys_mmap(2 * 8)) as *i64
357 let san_lens: *i64 = (sys_mmap(2 * 8)) as *i64
358 san_ptrs[0] = ("andelinwest.com" as *u8) as i64
359 san_lens[0] = 15
360 san_ptrs[1] = ("www.andelinwest.com" as *u8) as i64
361 san_lens[1] = 19
362
363 // Demo path 1: Ed25519
364 let rc_ed: i64 = nx_cert_gen_self_signed(
365 domain, domain_n,
366 san_ptrs, san_lens, 2,
367 NX_CG_DEFAULT_VALIDITY_DAYS,
368 "/tmp/nx_cert_gen_demo_ed25519_cert.der" as *u8,
369 "/tmp/nx_cert_gen_demo_ed25519_priv.bin" as *u8,
370 "/tmp/nx_cert_gen_demo_ed25519_pub.bin" as *u8)
371
372 if rc_ed != NX_CG_OK {
373 let err: *u8 = "ED25519 cert gen FAILED\n" as *u8
374 sys_write(2, err, 24)
375 return rc_ed
376 }
377 let msg_ed: *u8 = "ED25519 cert + priv + pub written to /tmp/nx_cert_gen_demo_ed25519_*\n" as *u8
378 sys_write(1, msg_ed, 68)
379
380 // Demo path 2: ECDSA P-256/SHA-256 (V-HOST-4d-2)
381 let rc_ec: i64 = nx_cert_gen_self_signed_ecdsa_p256(
382 domain, domain_n,
383 san_ptrs, san_lens, 2,
384 NX_CG_DEFAULT_VALIDITY_DAYS,
385 "/tmp/nx_cert_gen_demo_p256_cert.der" as *u8,
386 "/tmp/nx_cert_gen_demo_p256_priv.bin" as *u8,
387 "/tmp/nx_cert_gen_demo_p256_pub.bin" as *u8)
388
389 if rc_ec != NX_CG_OK {
390 let err: *u8 = "ECDSA P-256 cert gen FAILED\n" as *u8
391 sys_write(2, err, 28)
392 return rc_ec
393 }
394 let msg_ec: *u8 = "ECDSA P-256 cert + priv + pub written to /tmp/nx_cert_gen_demo_p256_*\n" as *u8
395 sys_write(1, msg_ec, 69)
396
397 return 0
398}