code wiki / bin / nx_cert_gen.nx

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}