nx_x509_build.nx source
↩ module page · 603 lines · 26693 B
1// nx_x509_build.nx -- V-HOST-4b: sovereign X.509 v3 self-signed cert builder.
2//
3// Composes hub/nx_asn1_write (V-HOST-4a) + ed25519_sign_full to produce
4// a fully-conformant RFC 5280 X.509 v3 certificate signed with Ed25519
5// (RFC 8410 + RFC 8032).
6//
7// COMPOSES (per "avoid duplicate primitives"):
8// hub/nx_asn1_write (DER encoder; sibling to existing asn1.nx READ surface)
9// nx_ed25519_signature (ed25519_sign_full per RFC 8032 §5.1.6)
10//
11// COMPOSED BY:
12// bin/nx_cert_gen.nx (V-HOST-4c; CLI driver; queued)
13// future: ACME cert request flow integration
14//
15// V-HOST-4b SCOPE per RFC 5280:
16// Certificate {
17// tbsCertificate TBSCertificate,
18// signatureAlgorithm AlgorithmIdentifier, // ed25519
19// signatureValue BIT STRING // ed25519 64-byte sig
20// }
21// TBSCertificate {
22// version [0] EXPLICIT INTEGER (v3 = 2),
23// serialNumber INTEGER (operator-supplied OR random),
24// signature AlgorithmIdentifier, // ed25519
25// issuer Name, // self-signed: subject == issuer
26// validity Validity {notBefore, notAfter}, // GeneralizedTime
27// subject Name, // CN=<domain>
28// subjectPublicKeyInfo SubjectPublicKeyInfo,// ed25519 pubkey
29// extensions [3] EXPLICIT Extensions { // V3 extensions
30// subjectAltName // SAN list
31// basicConstraints // CA:FALSE
32// keyUsage // digitalSignature
33// }
34// }
35//
36// V-HOST-4b NON-SCOPE (V+1):
37// - ECDSA / RSA sig algs (only Ed25519 V1)
38// - CA certs (only end-entity / leaf V1)
39// - Cert chain (only self-signed V1)
40// - CRL distribution points / OCSP / AIA extensions
41//
42// Status: V-HOST-4b. 2026-05-27.
43
44import "nx_syscalls.nx"
45import "hub/nx_asn1_write.nx"
46import "nx_ed25519_signature.nx"
47// V-HOST-4d-1: ECDSA P-256 path additions
48import "nx_ecdsa_p256.nx"
49import "nx_ecdsa_p256_sign.nx"
50import "nx_x509_sig_alg.nx"
51import "nx_u256.nx"
52import "nx_p256_point.nx"
53import "nx_sha256.nx"
54
55// ===== Sealed verdict surface (codes 3620-3634) =================================================
56const NX_X509_OK: i64 = 0
57const NX_X509_BAD_INPUT: i64 = 3620
58const NX_X509_BUF_OVERFLOW: i64 = 3621
59const NX_X509_TOO_MANY_SANS: i64 = 3622
60const NX_X509_BAD_TIME: i64 = 3623
61const NX_X509_SIGN_FAILED: i64 = 3624
62const NX_X509_SERIAL_TOO_LARGE: i64 = 3625
63
64// ===== Named constants (M7) =================================================
65const NX_X509_MAX_DER_OUT: i64 = 8192 // V1 self-signed cert easily fits
66const NX_X509_MAX_TBS_BUF: i64 = 4096 // TBSCertificate scratch
67const NX_X509_MAX_NAME_BUF: i64 = 256 // CN body
68const NX_X509_MAX_SANS: i64 = 16
69const NX_X509_MAX_SAN_LEN: i64 = 253 // hostname per RFC 1035 + room
70const NX_X509_MAX_VALIDITY_BUF: i64 = 64
71const NX_X509_MAX_SPKI_BUF: i64 = 128
72const NX_X509_MAX_EXT_BUF: i64 = 512
73const NX_X509_MAX_ISSUER_BUF: i64 = 256
74const NX_X509_ED25519_SIG_LEN: i64 = 64
75const NX_X509_ED25519_PUB_LEN: i64 = 32
76const NX_X509_ED25519_PRIV_LEN: i64 = 32
77
78// ===== Sealed sig-alg enum (V-HOST-4d-1) =================================================
79//
80// Per RFC 5280 + RFC 5480 + RFC 8410: cert can be signed with various
81// algorithms. V1 supports two sovereign options:
82// ED25519 -- RFC 8410 (Ed25519 sig; sovereign + modern)
83// ECDSA_P256_SHA256 -- RFC 5480 §2.1.1 (ECDSA with SHA-256; legacy-compat)
84
85// Signature-algorithm enum: use the ONE canonical definition from
86// nx_x509_sig_alg.nx (UNKNOWN=0, ED25519=1, ECDSA_P256_SHA256=2,
87// ECDSA_SHA384=3, RSA_PKCS1_SHA256=4, N=5). This file previously
88// redefined a SHORTER enum (NONE=0, ED25519=1, ECDSA_P256_SHA256=2,
89// N=3) -- a duplicate-const trap: when both this builder and the
90// validator (nx_x509_sig_alg) landed in one binary, the native
91// compiler silently resolved NX_X509_SIG_ALG_N to 3, breaking the
92// validator's is_supported() bound (alg>=3 -> ECDSA_SHA384 and
93// RSA_PKCS1_SHA256 wrongly rejected -> every LE chain failed). The
94// builder only ever uses ED25519 + ECDSA_P256_SHA256 (same values in
95// both enums), so importing the canonical enum is behavior-preserving.
96
97func nx_x509_sig_alg_name(a: i64) -> *u8 {
98 if a == NX_X509_SIG_ALG_ED25519 { return "ed25519" as *u8 }
99 if a == NX_X509_SIG_ALG_ECDSA_P256_SHA256 { return "ecdsa-p256-sha256" as *u8 }
100 return "UNKNOWN" as *u8
101}
102
103// ===== Sealed OID byte sequences (DER-encoded; per X.690 §8.19) =================================================
104//
105// Each OID's body (length-prefix handled by nx_aw_put_oid).
106// Pre-computed per OID arc decomposition:
107// 1.3.101.112 (Ed25519) = 0x2B 0x65 0x70
108// 2.5.29.17 (subjectAltName) = 0x55 0x1D 0x11
109// 2.5.29.19 (basicConstraints) = 0x55 0x1D 0x13
110// 2.5.29.15 (keyUsage) = 0x55 0x1D 0x0F
111// 2.5.4.3 (CN commonName) = 0x55 0x04 0x03
112
113const NX_X509_OID_ED25519: *u8 = "\x2B\x65\x70" as *u8
114const NX_X509_OID_ED25519_N: i64 = 3
115const NX_X509_OID_SAN: *u8 = "\x55\x1D\x11" as *u8
116const NX_X509_OID_SAN_N: i64 = 3
117const NX_X509_OID_BASIC_CONSTRAINTS: *u8 = "\x55\x1D\x13" as *u8
118const NX_X509_OID_BASIC_CONSTRAINTS_N: i64 = 3
119const NX_X509_OID_KEY_USAGE: *u8 = "\x55\x1D\x0F" as *u8
120const NX_X509_OID_KEY_USAGE_N: i64 = 3
121const NX_X509_OID_CN: *u8 = "\x55\x04\x03" as *u8
122const NX_X509_OID_CN_N: i64 = 3
123
124// V-HOST-4d-1 ECDSA OIDs (per RFC 5480 + ANSI X9.62):
125// ecdsa-with-SHA256 = 1.2.840.10045.4.3.2 -> 0x2A 0x86 0x48 0xCE 0x3D 0x04 0x03 0x02
126const NX_X509_OID_ECDSA_SHA256: *u8 = "\x2A\x86\x48\xCE\x3D\x04\x03\x02" as *u8
127const NX_X509_OID_ECDSA_SHA256_N: i64 = 8
128// id-ecPublicKey = 1.2.840.10045.2.1 -> 0x2A 0x86 0x48 0xCE 0x3D 0x02 0x01
129const NX_X509_OID_EC_PUBLIC_KEY: *u8 = "\x2A\x86\x48\xCE\x3D\x02\x01" as *u8
130const NX_X509_OID_EC_PUBLIC_KEY_N: i64 = 7
131// secp256r1 (P-256 namedCurve) = 1.2.840.10045.3.1.7 -> 0x2A 0x86 0x48 0xCE 0x3D 0x03 0x01 0x07
132const NX_X509_OID_SECP256R1: *u8 = "\x2A\x86\x48\xCE\x3D\x03\x01\x07" as *u8
133const NX_X509_OID_SECP256R1_N: i64 = 8
134
135const NX_X509_ECDSA_P256_PUB_LEN: i64 = 65 // 0x04 || X(32B) || Y(32B) uncompressed
136
137// ===== Cert build inputs (caller-supplied) =================================================
138
139struct NxX509BuildInputs {
140 // Subject (== issuer for self-signed)
141 cn: *u8 // common name (the primary domain)
142 cn_n: i64
143 // SAN list (parallel arrays; san_ptrs[i] + san_lens[i])
144 san_ptrs: *i64
145 san_lens: *i64
146 san_count: i64
147 // Ed25519 keypair
148 priv_32: *u8
149 pub_32: *u8
150 // Validity (operator-supplied GeneralizedTime strings: YYYYMMDDhhmmssZ)
151 not_before: *u8 // 15 bytes
152 not_after: *u8 // 15 bytes
153 // Serial number (1..8 bytes; positive)
154 serial: i64
155 // V-HOST-4d-1: signature algorithm selector
156 sig_alg: i64 // NX_X509_SIG_ALG_* enum
157 // V-HOST-4d-1: ECDSA P-256 pubkey (X || Y as i64 limb arrays; 4 limbs each)
158 // Only used when sig_alg == ECDSA_P256_SHA256; ignored otherwise.
159 ecdsa_pub_x: *i64 // 4 limbs (256-bit X coordinate)
160 ecdsa_pub_y: *i64 // 4 limbs (256-bit Y coordinate)
161 // V-HOST-4d-1: ECDSA P-256 priv as 4-limb i64
162 ecdsa_priv_limbs: *i64 // 4 limbs (256-bit scalar)
163 valid: i64
164}
165
166func nx_x509_inputs_init(inp: *NxX509BuildInputs,
167 cn: *u8, cn_n: i64,
168 san_ptrs: *i64, san_lens: *i64, san_count: i64,
169 priv_32: *u8, pub_32: *u8,
170 not_before: *u8, not_after: *u8,
171 serial: i64) -> i64 {
172 if (inp as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
173 if (cn as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
174 if cn_n < 1 { return 0 - NX_X509_BAD_INPUT }
175 if cn_n > NX_X509_MAX_NAME_BUF { return 0 - NX_X509_BAD_INPUT }
176 if san_count < 0 { return 0 - NX_X509_BAD_INPUT }
177 if san_count > NX_X509_MAX_SANS { return 0 - NX_X509_TOO_MANY_SANS }
178 if (priv_32 as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
179 if (pub_32 as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
180 if (not_before as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
181 if (not_after as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
182 if serial < 1 { return 0 - NX_X509_BAD_INPUT }
183
184 inp.cn = cn; inp.cn_n = cn_n
185 inp.san_ptrs = san_ptrs; inp.san_lens = san_lens; inp.san_count = san_count
186 inp.priv_32 = priv_32; inp.pub_32 = pub_32
187 inp.not_before = not_before; inp.not_after = not_after
188 inp.serial = serial
189 // V-HOST-4d-1: backward-compat default = ED25519 (matches original V-HOST-4 behavior)
190 inp.sig_alg = NX_X509_SIG_ALG_ED25519
191 inp.ecdsa_pub_x = 0 as *i64
192 inp.ecdsa_pub_y = 0 as *i64
193 inp.ecdsa_priv_limbs = 0 as *i64
194 inp.valid = 1
195 return NX_X509_OK
196}
197
198// V-HOST-4d-1: ECDSA P-256 inputs init (parallel to nx_x509_inputs_init).
199// Caller supplies P-256 pub coordinates + priv as 4-limb i64 arrays.
200// priv_32 + pub_32 fields ignored for ECDSA path.
201func nx_x509_inputs_init_ecdsa_p256(inp: *NxX509BuildInputs,
202 cn: *u8, cn_n: i64,
203 san_ptrs: *i64, san_lens: *i64, san_count: i64,
204 priv_limbs: *i64,
205 pub_x_limbs: *i64, pub_y_limbs: *i64,
206 not_before: *u8, not_after: *u8,
207 serial: i64) -> i64 {
208 if (inp as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
209 if (cn as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
210 if cn_n < 1 { return 0 - NX_X509_BAD_INPUT }
211 if cn_n > NX_X509_MAX_NAME_BUF { return 0 - NX_X509_BAD_INPUT }
212 if san_count < 0 { return 0 - NX_X509_BAD_INPUT }
213 if san_count > NX_X509_MAX_SANS { return 0 - NX_X509_TOO_MANY_SANS }
214 if (priv_limbs as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
215 if (pub_x_limbs as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
216 if (pub_y_limbs as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
217 if (not_before as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
218 if (not_after as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
219 if serial < 1 { return 0 - NX_X509_BAD_INPUT }
220
221 inp.cn = cn; inp.cn_n = cn_n
222 inp.san_ptrs = san_ptrs; inp.san_lens = san_lens; inp.san_count = san_count
223 inp.priv_32 = 0 as *u8
224 inp.pub_32 = 0 as *u8
225 inp.not_before = not_before; inp.not_after = not_after
226 inp.serial = serial
227 inp.sig_alg = NX_X509_SIG_ALG_ECDSA_P256_SHA256
228 inp.ecdsa_pub_x = pub_x_limbs
229 inp.ecdsa_pub_y = pub_y_limbs
230 inp.ecdsa_priv_limbs = priv_limbs
231 inp.valid = 1
232 return NX_X509_OK
233}
234
235// ===== Build AlgorithmIdentifier for Ed25519 =================================================
236//
237// AlgorithmIdentifier { algorithm = OID(1.3.101.112), parameters = ABSENT }
238// per RFC 8410 §3 (Ed25519 OIDs omit parameters; not NULL).
239
240func nx_x509_build_alg_ed25519(out: *u8, cap: i64, off: i64) -> i64 {
241 let scratch: *u8 = sys_mmap(64)
242 var so: i64 = 0
243 so = nx_aw_put_oid(scratch, 64, so, NX_X509_OID_ED25519, NX_X509_OID_ED25519_N)
244 if so < 0 { return so }
245 // No parameters per RFC 8410 §3 -- wrap inp SEQUENCE
246 return nx_aw_wrap_sequence(out, cap, off, scratch, so)
247}
248
249// ===== V-HOST-4d-1: Build AlgorithmIdentifier for ECDSA-with-SHA256 =================================================
250//
251// AlgorithmIdentifier { algorithm = OID(1.2.840.10045.4.3.2), parameters = ABSENT }
252// per RFC 5758 §3.2 (ecdsa-with-SHA256 has no parameters).
253
254func nx_x509_build_alg_ecdsa_p256_sha256(out: *u8, cap: i64, off: i64) -> i64 {
255 let scratch: *u8 = sys_mmap(64)
256 var so: i64 = 0
257 so = nx_aw_put_oid(scratch, 64, so, NX_X509_OID_ECDSA_SHA256, NX_X509_OID_ECDSA_SHA256_N)
258 if so < 0 { return so }
259 // No parameters per RFC 5758 §3.2 -- wrap inp SEQUENCE
260 return nx_aw_wrap_sequence(out, cap, off, scratch, so)
261}
262
263// ===== V-HOST-4d-1: Sig-alg-dispatched AlgorithmIdentifier =================================================
264//
265// Switches on inp.sig_alg to emit the correct AlgorithmIdentifier.
266
267func nx_x509_build_sig_alg(out: *u8, cap: i64, off: i64, sig_alg: i64) -> i64 {
268 if sig_alg == NX_X509_SIG_ALG_ED25519 {
269 return nx_x509_build_alg_ed25519(out, cap, off)
270 }
271 if sig_alg == NX_X509_SIG_ALG_ECDSA_P256_SHA256 {
272 return nx_x509_build_alg_ecdsa_p256_sha256(out, cap, off)
273 }
274 return 0 - NX_X509_BAD_INPUT
275}
276
277// ===== V-HOST-4d-1: Build SubjectPublicKeyInfo for ECDSA P-256 =================================================
278//
279// SubjectPublicKeyInfo {
280// algorithm = SEQUENCE {
281// algorithm = OID(id-ecPublicKey),
282// parameters = OID(secp256r1) -- named curve
283// },
284// subjectPublicKey = BIT STRING (65 bytes: 0x04 || X(32B) || Y(32B) uncompressed)
285// }
286// per RFC 5480 §2
287
288func nx_x509_build_spki_ecdsa_p256(out: *u8, cap: i64, off: i64,
289 pub_x: *i64, pub_y: *i64) -> i64 {
290 // 1. Build inner AlgorithmIdentifier SEQUENCE {ecPublicKey OID, secp256r1 OID}
291 let alg_body: *u8 = sys_mmap(64)
292 var ao: i64 = 0
293 ao = nx_aw_put_oid(alg_body, 64, ao, NX_X509_OID_EC_PUBLIC_KEY, NX_X509_OID_EC_PUBLIC_KEY_N)
294 if ao < 0 { return ao }
295 ao = nx_aw_put_oid(alg_body, 64, ao, NX_X509_OID_SECP256R1, NX_X509_OID_SECP256R1_N)
296 if ao < 0 { return ao }
297 let alg_seq: *u8 = sys_mmap(64)
298 let alg_seq_n: i64 = nx_aw_wrap_sequence(alg_seq, 64, 0, alg_body, ao)
299 if alg_seq_n < 0 { return alg_seq_n }
300
301 // 2. Build uncompressed point: 0x04 || X(32B) || Y(32B)
302 let pub_bytes: *u8 = sys_mmap(65)
303 pub_bytes[0] = 0x04 as u8
304 let x_off: *u8 = ((pub_bytes as i64) + 1) as *u8
305 let y_off: *u8 = ((pub_bytes as i64) + 33) as *u8
306 u256_store_be(x_off, pub_x)
307 u256_store_be(y_off, pub_y)
308
309 // 3. Build SubjectPublicKeyInfo body: alg + BIT STRING(pub)
310 let spki_body: *u8 = sys_mmap(NX_X509_MAX_SPKI_BUF)
311 var sbo: i64 = 0
312 sbo = nx_aw_put_raw(spki_body, NX_X509_MAX_SPKI_BUF, sbo, alg_seq, alg_seq_n)
313 if sbo < 0 { return sbo }
314 sbo = nx_aw_put_bit_string(spki_body, NX_X509_MAX_SPKI_BUF, sbo, 0, pub_bytes, 65)
315 if sbo < 0 { return sbo }
316
317 return nx_aw_wrap_sequence(out, cap, off, spki_body, sbo)
318}
319
320// ===== V-HOST-4d-1: Sig-alg-dispatched SPKI =================================================
321
322func nx_x509_build_spki(out: *u8, cap: i64, off: i64, inp: *NxX509BuildInputs) -> i64 {
323 if inp.sig_alg == NX_X509_SIG_ALG_ED25519 {
324 return nx_x509_build_spki_ed25519(out, cap, off, inp.pub_32)
325 }
326 if inp.sig_alg == NX_X509_SIG_ALG_ECDSA_P256_SHA256 {
327 return nx_x509_build_spki_ecdsa_p256(out, cap, off, inp.ecdsa_pub_x, inp.ecdsa_pub_y)
328 }
329 return 0 - NX_X509_BAD_INPUT
330}
331
332// ===== V-HOST-4d-1: Encode ECDSA (r, s) as DER SEQUENCE {INTEGER r, INTEGER s} =================================================
333//
334// Per ANSI X9.62 / RFC 3279 §2.2.3: ECDSA signature value is
335// Ecdsa-Sig-Value ::= SEQUENCE {r INTEGER, s INTEGER}
336// Composes nx_asn1_write.nx_aw_put_integer_bytes (auto-prepends 0x00
337// when MSB high to keep INTEGERs unambiguously positive per DER).
338
339func nx_x509_ecdsa_sig_to_der(r: *i64, s: *i64,
340 out: *u8, cap: i64) -> i64 {
341 // 1. Convert r, s limbs to big-endian byte arrays (32 bytes each)
342 let r_be: *u8 = sys_mmap(32)
343 let s_be: *u8 = sys_mmap(32)
344 u256_store_be(r_be, r)
345 u256_store_be(s_be, s)
346
347 // 2. Strip leading zero bytes to the minimal big-endian magnitude,
348 // keeping at least one byte (an all-zero value encodes as 0x00).
349 // nx_aw_put_integer_bytes re-prepends 0x00 when the MSB is set, so
350 // we must NOT leave a spurious leading zero here.
351 var r_off: i64 = 0
352 while r_off < 31 {
353 if r_be[r_off] != (0 as u8) { break }
354 r_off = r_off + 1
355 }
356 var s_off: i64 = 0
357 while s_off < 31 {
358 if s_be[s_off] != (0 as u8) { break }
359 s_off = s_off + 1
360 }
361 let r_trimmed: *u8 = ((r_be as i64) + r_off) as *u8
362 let r_trim_n: i64 = 32 - r_off
363 let s_trimmed: *u8 = ((s_be as i64) + s_off) as *u8
364 let s_trim_n: i64 = 32 - s_off
365
366 // 3. Build SEQUENCE body: INTEGER r + INTEGER s
367 let body: *u8 = sys_mmap(96)
368 var bo: i64 = 0
369 bo = nx_aw_put_integer_bytes(body, 96, bo, r_trimmed, r_trim_n); if bo < 0 { return bo }
370 bo = nx_aw_put_integer_bytes(body, 96, bo, s_trimmed, s_trim_n); if bo < 0 { return bo }
371
372 // 4. Wrap as SEQUENCE
373 return nx_aw_wrap_sequence(out, cap, 0, body, bo)
374}
375
376// ===== Build Name (V1: just CN) =================================================
377//
378// Name = SEQUENCE OF RelativeDistinguishedName
379// RDN = SET OF AttributeTypeAndValue
380// ATV = SEQUENCE {type OID, value UTF8String}
381
382func nx_x509_build_name_cn(out: *u8, cap: i64, off: i64,
383 cn: *u8, cn_n: i64) -> i64 {
384 let atv_buf: *u8 = sys_mmap(NX_X509_MAX_NAME_BUF)
385 var ao: i64 = 0
386 ao = nx_aw_put_oid(atv_buf, NX_X509_MAX_NAME_BUF, ao, NX_X509_OID_CN, NX_X509_OID_CN_N); if ao < 0 { return ao }
387 ao = nx_aw_put_utf8_string(atv_buf, NX_X509_MAX_NAME_BUF, ao, cn, cn_n); if ao < 0 { return ao }
388
389 // Wrap ATV inp SEQUENCE
390 let atv_seq_buf: *u8 = sys_mmap(NX_X509_MAX_NAME_BUF)
391 let atv_seq_n: i64 = nx_aw_wrap_sequence(atv_seq_buf, NX_X509_MAX_NAME_BUF, 0, atv_buf, ao)
392 if atv_seq_n < 0 { return atv_seq_n }
393
394 // Wrap inp SET (RDN)
395 let rdn_buf: *u8 = sys_mmap(NX_X509_MAX_NAME_BUF)
396 let rdn_n: i64 = nx_aw_wrap_set(rdn_buf, NX_X509_MAX_NAME_BUF, 0, atv_seq_buf, atv_seq_n)
397 if rdn_n < 0 { return rdn_n }
398
399 // Wrap RDN inp SEQUENCE (Name = SEQUENCE OF RDN)
400 return nx_aw_wrap_sequence(out, cap, off, rdn_buf, rdn_n)
401}
402
403// ===== Build Validity =================================================
404
405func nx_x509_build_validity(out: *u8, cap: i64, off: i64,
406 not_before: *u8, not_after: *u8) -> i64 {
407 let scratch: *u8 = sys_mmap(NX_X509_MAX_VALIDITY_BUF)
408 var so: i64 = 0
409 so = nx_aw_put_generalized_time(scratch, NX_X509_MAX_VALIDITY_BUF, so, not_before)
410 if so < 0 { return so }
411 so = nx_aw_put_generalized_time(scratch, NX_X509_MAX_VALIDITY_BUF, so, not_after)
412 if so < 0 { return so }
413 return nx_aw_wrap_sequence(out, cap, off, scratch, so)
414}
415
416// ===== Build SubjectPublicKeyInfo (Ed25519) =================================================
417//
418// SubjectPublicKeyInfo {
419// algorithm = AlgorithmIdentifier(Ed25519),
420// subjectPublicKey = BIT STRING(32-byte Ed25519 pubkey; unused_bits = 0)
421// }
422
423func nx_x509_build_spki_ed25519(out: *u8, cap: i64, off: i64,
424 pub_32: *u8) -> i64 {
425 let scratch: *u8 = sys_mmap(NX_X509_MAX_SPKI_BUF)
426 var so: i64 = 0
427 so = nx_x509_build_alg_ed25519(scratch, NX_X509_MAX_SPKI_BUF, so); if so < 0 { return so }
428 so = nx_aw_put_bit_string(scratch, NX_X509_MAX_SPKI_BUF, so, 0, pub_32, NX_X509_ED25519_PUB_LEN)
429 if so < 0 { return so }
430 return nx_aw_wrap_sequence(out, cap, off, scratch, so)
431}
432
433// ===== Build Extensions =================================================
434
435// SAN extension: extnValue is OCTET STRING containing DER-encoded
436// SubjectAltName ::= SEQUENCE OF GeneralName.
437// GeneralName.dNSName = [2] IMPLICIT IA5String.
438
439func nx_x509_build_ext_san(out: *u8, cap: i64, off: i64,
440 san_ptrs: *i64, san_lens: *i64, san_count: i64) -> i64 {
441 // 1. Build SubjectAltName body: SEQUENCE OF [2] IMPLICIT IA5String
442 let san_body: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
443 var sbo: i64 = 0
444 var i: i64 = 0
445 while i < san_count {
446 let san_ptr: *u8 = san_ptrs[i] as *u8
447 let san_n: i64 = san_lens[i]
448 if san_n > NX_X509_MAX_SAN_LEN { return 0 - NX_X509_BAD_INPUT }
449 sbo = nx_aw_wrap_implicit_primitive(san_body, NX_X509_MAX_EXT_BUF, sbo,
450 2, san_ptr, san_n)
451 if sbo < 0 { return sbo }
452 i = i + 1
453 }
454 // Wrap as SEQUENCE
455 let san_seq: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
456 let san_seq_n: i64 = nx_aw_wrap_sequence(san_seq, NX_X509_MAX_EXT_BUF, 0, san_body, sbo)
457 if san_seq_n < 0 { return san_seq_n }
458
459 // 2. Wrap as OCTET STRING (extnValue)
460 let octet_buf: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
461 let octet_n: i64 = nx_aw_put_octet_string(octet_buf, NX_X509_MAX_EXT_BUF, 0, san_seq, san_seq_n)
462 if octet_n < 0 { return octet_n }
463
464 // 3. Build Extension = SEQUENCE {extnID OID, extnValue OCTET STRING}
465 let ext_body: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
466 var ebo: i64 = 0
467 ebo = nx_aw_put_oid(ext_body, NX_X509_MAX_EXT_BUF, ebo, NX_X509_OID_SAN, NX_X509_OID_SAN_N)
468 if ebo < 0 { return ebo }
469 ebo = nx_aw_put_raw(ext_body, NX_X509_MAX_EXT_BUF, ebo, octet_buf, octet_n)
470 if ebo < 0 { return ebo }
471
472 return nx_aw_wrap_sequence(out, cap, off, ext_body, ebo)
473}
474
475// Build full Extensions block: [3] EXPLICIT SEQUENCE OF Extension
476func nx_x509_build_extensions(out: *u8, cap: i64, off: i64,
477 inp: *NxX509BuildInputs) -> i64 {
478 let ext_seq_body: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
479 var ebo: i64 = 0
480
481 // SAN extension (only V1 extension; basicConstraints + keyUsage queued V+1)
482 if inp.san_count > 0 {
483 ebo = nx_x509_build_ext_san(ext_seq_body, NX_X509_MAX_EXT_BUF, ebo,
484 inp.san_ptrs, inp.san_lens, inp.san_count)
485 if ebo < 0 { return ebo }
486 }
487 if ebo == 0 { return off } // no extensions; caller skips the [3] wrap
488
489 // Wrap as SEQUENCE OF Extension
490 let ext_seq: *u8 = sys_mmap(NX_X509_MAX_EXT_BUF)
491 let ext_seq_n: i64 = nx_aw_wrap_sequence(ext_seq, NX_X509_MAX_EXT_BUF, 0, ext_seq_body, ebo)
492 if ext_seq_n < 0 { return ext_seq_n }
493
494 // Wrap inp [3] EXPLICIT
495 return nx_aw_wrap_explicit(out, cap, off, 3, ext_seq, ext_seq_n)
496}
497
498// ===== Build TBSCertificate =================================================
499
500func nx_x509_build_tbs(out: *u8, cap: i64, off: i64,
501 inp: *NxX509BuildInputs) -> i64 {
502 let tbs_body: *u8 = sys_mmap(NX_X509_MAX_TBS_BUF)
503 var bo: i64 = 0
504
505 // version [0] EXPLICIT INTEGER (v3 == 2)
506 let ver_buf: *u8 = sys_mmap(8)
507 let ver_n: i64 = nx_aw_put_integer(ver_buf, 8, 0, 2)
508 if ver_n < 0 { return ver_n }
509 bo = nx_aw_wrap_explicit(tbs_body, NX_X509_MAX_TBS_BUF, bo, 0, ver_buf, ver_n)
510 if bo < 0 { return bo }
511
512 // serialNumber INTEGER
513 bo = nx_aw_put_integer(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp.serial); if bo < 0 { return bo }
514
515 // signature AlgorithmIdentifier (sig_alg-dispatched per V-HOST-4d-1)
516 bo = nx_x509_build_sig_alg(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp.sig_alg); if bo < 0 { return bo }
517
518 // issuer Name (== subject for self-signed)
519 bo = nx_x509_build_name_cn(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp.cn, inp.cn_n)
520 if bo < 0 { return bo }
521
522 // validity
523 bo = nx_x509_build_validity(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp.not_before, inp.not_after)
524 if bo < 0 { return bo }
525
526 // subject Name
527 bo = nx_x509_build_name_cn(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp.cn, inp.cn_n)
528 if bo < 0 { return bo }
529
530 // subjectPublicKeyInfo (sig_alg-dispatched per V-HOST-4d-1)
531 bo = nx_x509_build_spki(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp)
532 if bo < 0 { return bo }
533
534 // extensions [3] EXPLICIT (V1: SAN only if present)
535 bo = nx_x509_build_extensions(tbs_body, NX_X509_MAX_TBS_BUF, bo, inp)
536 if bo < 0 { return bo }
537
538 // Wrap as TBSCertificate SEQUENCE
539 return nx_aw_wrap_sequence(out, cap, off, tbs_body, bo)
540}
541
542// ===== Build full self-signed Certificate =================================================
543//
544// Certificate { tbsCertificate, signatureAlgorithm, signatureValue }
545
546func nx_x509_build_self_signed(inp: *NxX509BuildInputs,
547 out: *u8, cap: i64, out_n: *i64) -> i64 {
548 if inp.valid != 1 { return 0 - NX_X509_BAD_INPUT }
549 if (out as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
550 if cap < 256 { return 0 - NX_X509_BUF_OVERFLOW }
551 if (out_n as i64) == 0 { return 0 - NX_X509_BAD_INPUT }
552
553 // 1. Build TBSCertificate to scratch
554 let tbs_buf: *u8 = sys_mmap(NX_X509_MAX_DER_OUT)
555 let tbs_n: i64 = nx_x509_build_tbs(tbs_buf, NX_X509_MAX_DER_OUT, 0, inp)
556 if tbs_n < 0 { return tbs_n }
557
558 // 2. Sign TBSCertificate (sig_alg-dispatched per V-HOST-4d-1)
559 let sig_buf: *u8 = sys_mmap(128) // Ed25519=64; ECDSA-DER up to ~72
560 let sig_n_p: *i64 = (sys_mmap(8)) as *i64
561 sig_n_p[0] = 0
562
563 if inp.sig_alg == NX_X509_SIG_ALG_ED25519 {
564 let sign_rc: i64 = ed25519_sign_full(inp.priv_32, tbs_buf, tbs_n, sig_buf)
565 if sign_rc != 0 { return 0 - NX_X509_SIGN_FAILED }
566 sig_n_p[0] = NX_X509_ED25519_SIG_LEN
567 }
568 if inp.sig_alg == NX_X509_SIG_ALG_ECDSA_P256_SHA256 {
569 // a. Hash TBS with SHA-256
570 let h: *u8 = sys_mmap(32)
571 sha256_digest(tbs_buf, tbs_n, h)
572 // b. Load hash into u256 (mod n -- nx_ecdsa_p256_sign handles reduction)
573 let h_limbs: *i64 = u256_alloc()
574 u256_load_be(h_limbs, h)
575 // c. Sign -> (r, s) u256
576 let r: *i64 = u256_alloc()
577 let s: *i64 = u256_alloc()
578 let sign_rc: i64 = nx_ecdsa_p256_sign(inp.ecdsa_priv_limbs, h_limbs, r, s)
579 if sign_rc != NX_ECDSA_OK { return 0 - NX_X509_SIGN_FAILED }
580 // d. DER-encode (r, s) into sig_buf
581 let der_n: i64 = nx_x509_ecdsa_sig_to_der(r, s, sig_buf, 128)
582 if der_n < 0 { return 0 - NX_X509_SIGN_FAILED }
583 sig_n_p[0] = der_n
584 }
585
586 // 3. Build outer Certificate SEQUENCE body:
587 // tbsCertificate (already encoded) +
588 // signatureAlgorithm (sig_alg-dispatched) +
589 // signatureValue (BIT STRING)
590 let cert_body: *u8 = sys_mmap(NX_X509_MAX_DER_OUT)
591 var co: i64 = 0
592 co = nx_aw_put_raw(cert_body, NX_X509_MAX_DER_OUT, co, tbs_buf, tbs_n); if co < 0 { return co }
593 co = nx_x509_build_sig_alg(cert_body, NX_X509_MAX_DER_OUT, co, inp.sig_alg); if co < 0 { return co }
594 co = nx_aw_put_bit_string(cert_body, NX_X509_MAX_DER_OUT, co, 0, sig_buf, sig_n_p[0])
595 if co < 0 { return co }
596
597 // 4. Wrap as outer Certificate SEQUENCE
598 let final_n: i64 = nx_aw_wrap_sequence(out, cap, 0, cert_body, co)
599 if final_n < 0 { return final_n }
600
601 out_n[0] = final_n
602 return NX_X509_OK
603}