nx_x509_verify_under_issuer.nx source
↩ module page · 291 lines · 14503 B
1// nx_x509_verify_under_issuer.nx -- per-cert outer-signature verify
2// against an issuer's pubkey, dispatching by sig_alg + pubkey_alg.
3//
4// Phase 0b §I.4 piece 5 of the chain-walker arc. This is the
5// per-cert step the chain walker repeats from leaf to root:
6// for each (cert, issuer) pair in the chain:
7// nx_x509_verify_under_issuer(buf, cert, issuer_buf, issuer_cert)
8//
9// Dispatch:
10// 1. Identify cert's outer sig_alg from cert.sig_alg_off/_len
11// 2. Identify issuer's pubkey_alg from issuer.pubkey_alg_off/_len
12// 3. Assert the sig_alg and pubkey_alg pair is consistent (e.g.,
13// ECDSA-P256-SHA-256 sig requires EC-P256 pubkey).
14// 4. Extract issuer's pubkey via the alg-specific extractor.
15// 5. Verify cert.tbs's outer sig under that pubkey via the
16// alg-specific verifier.
17//
18// Composes 6 substrate primitives:
19// nx_x509_sig_alg.nx (sig_alg OID identify)
20// nx_x509_pubkey_alg.nx (pubkey_alg OID identify)
21// nx_x509_pubkey_ec.nx (extract EC P-256 X, Y)
22// nx_x509_pubkey_ed.nx (extract Ed25519 32-byte key)
23// nx_x509_verify_ecdsa.nx (verify ECDSA-P256+SHA-256)
24// nx_x509_validate.nx (verify Ed25519 raw sig)
25//
26// Sealed verdict enum (one for the whole pipeline):
27// NX_X509_VUI_OK verify succeeded
28// NX_X509_VUI_SIG_ALG_UNSUPPORTED cert sig_alg not recognized
29// NX_X509_VUI_PUB_ALG_UNSUPPORTED issuer pubkey_alg not recognized
30// NX_X509_VUI_ALG_MISMATCH sig_alg requires different pubkey alg
31// NX_X509_VUI_PUBKEY_EXTRACT_FAIL pubkey extractor returned non-OK
32// NX_X509_VUI_VERIFY_FAIL verifier returned non-OK
33//
34// Per Cardinals 9 (single-responsibility -- this is JUST dispatch +
35// orchestration; each step has its own primitive), 12 (defensive
36// at boundaries -- algorithm pairing check is the boundary), 22
37// (composition -- 6 primitives compose), 23 (preamble names every
38// dispatch case).
39//
40// license_tier: INDEPENDENT_REDERIVE
41// genealogy_id: international-research-sources/ietf/rfc_5280 + rfc_5480 +
42// rfc_8410 + sec_g/sec1_v2
43// lineage_id: nishi_x509_verify_under_issuer_q10
44
45// nx_safety_envelope:
46// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
47// sil_target: SIL1
48// evidence: [bulk_applied_2026-05-19, x509-verify-under-issuer-dispatch]
49// verdict: NOT_YET_EVALUATED
50
51import "nx_syscalls.nx"
52import "nx_u256.nx"
53import "nx_u384.nx"
54import "nx_x509.nx"
55import "nx_x509_sig_alg.nx"
56import "nx_x509_pubkey_alg.nx"
57import "nx_x509_pubkey_ec.nx"
58import "nx_x509_pubkey_ed.nx"
59import "nx_x509_verify_ecdsa.nx"
60import "nx_x509_verify_ecdsa_p384.nx"
61import "nx_x509_verify_rsa_pkcs1_sha256.nx"
62import "nx_x509_pubkey_rsa.nx"
63import "nx_u2048.nx"
64// RSA-4096 WIRED 2026-05-28: needed to validate real Web PKI chains that
65// terminate at an RSA-4096 root (e.g. Let's Encrypt's ISRG Root X1, which
66// signs the ECDSA E-series intermediates). These modules were SHIPPED but
67// left unwired on 2026-05-20 because adding them pushed the C BOOTSTRAP
68// parser over its mconsts[2048] cap. We now build with the NATIVE
69// self-hosted compiler (nx_compile_x86_native), which does not have that
70// cap -- so the proper fix lands. Dispatch is by signature length (an RSA
71// sig equals the issuer modulus byte-length), keeping the 2048 path intact.
72import "nx_u4096.nx"
73import "nx_x509_pubkey_rsa_4096.nx"
74import "nx_x509_verify_rsa_pkcs1_sha256_4096.nx"
75import "nx_rsa_pkcs1_v1_5_sha384.nx"
76import "nx_rsa_pkcs1_v1_5_k.nx"
77import "nx_x509_validate.nx"
78import "nx_ed25519_signature.nx"
79const NX_MAGIC_4096: i64 = 4096
80const NX_MAGIC_2048: i64 = 2048
81
82const NX_X509_VUI_OK: i64 = 1
83const NX_X509_VUI_SIG_ALG_UNSUPPORTED: i64 = 2
84const NX_X509_VUI_PUB_ALG_UNSUPPORTED: i64 = 3
85const NX_X509_VUI_ALG_MISMATCH: i64 = 4
86const NX_X509_VUI_PUBKEY_EXTRACT_FAIL: i64 = 5
87const NX_X509_VUI_VERIFY_FAIL: i64 = 6
88const NX_X509_VUI_VERDICT_N: i64 = 7
89
90func nx_x509_vui_verdict_is_valid(v: i64) -> i64 {
91 if v < NX_X509_VUI_OK { return 0 }
92 if v >= NX_X509_VUI_VERDICT_N { return 0 }
93 return 1
94}
95
96// Verify cert's outer sig under issuer_cert's pubkey. Both certs
97// must have been parsed by x509_parse against their respective
98// buffers.
99//
100// Returns NX_X509_VUI_OK on success, or a non-OK verdict.
101func nx_x509_verify_under_issuer(buf: *u8, cert: *X509Cert,
102 issuer_buf: *u8, issuer_cert: *X509Cert) -> i64 {
103 // ---- Step 1+2: identify both algorithms ----
104 let sig_alg: i64 = nx_x509_sig_alg_identify(buf, cert.sig_alg_off, cert.sig_alg_len)
105 if nx_x509_sig_alg_is_supported(sig_alg) != 1 {
106 return NX_X509_VUI_SIG_ALG_UNSUPPORTED
107 }
108
109 let pub_alg: i64 = nx_x509_pubkey_alg_identify(issuer_buf,
110 issuer_cert.pubkey_alg_off,
111 issuer_cert.pubkey_alg_len)
112 if nx_x509_pubkey_alg_is_supported(pub_alg) != 1 {
113 return NX_X509_VUI_PUB_ALG_UNSUPPORTED
114 }
115
116 // ---- Step 3+4+5: dispatch by sig_alg ----
117 if sig_alg == NX_X509_SIG_ALG_ECDSA_P256_SHA256 {
118 if pub_alg != NX_X509_PUBKEY_ALG_EC_P256 {
119 return NX_X509_VUI_ALG_MISMATCH
120 }
121 let x: *i64 = u256_alloc()
122 let y: *i64 = u256_alloc()
123 let ex: i64 = nx_x509_pubkey_extract_ec_p256(issuer_buf, issuer_cert, x, y)
124 if ex != NX_X509_PUBKEY_EC_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
125
126 let tbs_ptr: *u8 = buf + cert.tbs_off
127 let sig_ptr: *u8 = buf + cert.sig_off
128 let v: i64 = nx_x509_verify_ecdsa_p256(tbs_ptr, cert.tbs_len,
129 sig_ptr, cert.sig_len,
130 x, y)
131 sys_munmap(x as *u8, 256); sys_munmap(y as *u8, 256) // S-CLASS certloop leak fix (EC P-256)
132 if v == NX_X509_ECDSA_OK { return NX_X509_VUI_OK }
133 return NX_X509_VUI_VERIFY_FAIL
134 }
135
136 if sig_alg == NX_X509_SIG_ALG_ECDSA_SHA384 {
137 // ECDSA-SHA384 sig_alg is curve-agnostic; the curve comes
138 // from the issuer's pubkey_alg. Only P-384 issuer pubkeys
139 // are wired here; a P-256 pubkey signing with SHA-384 is
140 // permitted by FIPS 186-5 but vanishingly rare in real
141 // Web PKI, so we surface ALG_MISMATCH for now.
142 if pub_alg != NX_X509_PUBKEY_ALG_EC_P384 {
143 return NX_X509_VUI_ALG_MISMATCH
144 }
145 let x: *i64 = u384_alloc()
146 let y: *i64 = u384_alloc()
147 let ex: i64 = nx_x509_pubkey_extract_ec_p384(issuer_buf, issuer_cert, x, y)
148 if ex != NX_X509_PUBKEY_EC_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
149
150 let tbs_ptr: *u8 = buf + cert.tbs_off
151 let sig_ptr: *u8 = buf + cert.sig_off
152 let v: i64 = nx_x509_verify_ecdsa_p384(tbs_ptr, cert.tbs_len,
153 sig_ptr, cert.sig_len,
154 x, y)
155 u384_free(x); u384_free(y) // S-CLASS certloop leak fix (EC P-384)
156 if v == NX_X509_ECDSA_P384_OK { return NX_X509_VUI_OK }
157 return NX_X509_VUI_VERIFY_FAIL
158 }
159
160 if sig_alg == NX_X509_SIG_ALG_RSA_PKCS1_SHA256 {
161 if pub_alg != NX_X509_PUBKEY_ALG_RSA {
162 return NX_X509_VUI_ALG_MISMATCH
163 }
164 // An RSA PKCS#1 signature is exactly the issuer's modulus
165 // byte-length: 256=2048, 512=4096, else (e.g. 384=3072-bit, as on
166 // Sectigo R36). Each size routed below; 256/512 paths unchanged.
167 if cert.sig_len == 512 {
168 let n4: *i64 = u4096_alloc()
169 let e4_p: *i64 = sys_mmap(16) as *i64
170 let ex4: i64 = nx_x509_pubkey_extract_rsa_4096(issuer_buf, issuer_cert, n4, e4_p)
171 if ex4 != NX_X509_PUBKEY_RSA_4096_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
172
173 let tbs4: *u8 = buf + cert.tbs_off
174 let sig4: *u8 = buf + cert.sig_off
175 let v4: i64 = nx_x509_verify_rsa_pkcs1_sha256_4096(tbs4, cert.tbs_len,
176 sig4, cert.sig_len,
177 n4, *e4_p)
178 u4096_free(n4); sys_munmap(e4_p as *u8, 16) // S-CLASS certloop leak fix: free per-link big-ints
179 if v4 == NX_X509_RSA_PKCS1_SHA256_4096_OK { return NX_X509_VUI_OK }
180 return NX_X509_VUI_VERIFY_FAIL
181 }
182 if cert.sig_len > 256 {
183 // 257..511 bytes => non-power-of-two RSA modulus (3072 = 384).
184 // k-aware verify over the u4096 substrate (modulus+sig zero-extended).
185 let nk: *i64 = u4096_alloc()
186 let ek_p: *i64 = sys_mmap(16) as *i64
187 let exk: i64 = nx_x509_pubkey_extract_rsa_4096(issuer_buf, issuer_cert, nk, ek_p)
188 if exk != NX_X509_PUBKEY_RSA_4096_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
189 let tbsk: *u8 = buf + cert.tbs_off
190 let sigk: *u8 = buf + cert.sig_off
191 let vk: i64 = rsa_pkcs1_v1_5_sha256_k_verify(tbsk, cert.tbs_len,
192 sigk, cert.sig_len, nk, *ek_p)
193 u4096_free(nk); sys_munmap(ek_p as *u8, 16) // S-CLASS certloop leak fix
194 if vk == NX_RSA_PKCS1_V15_384_OK { return NX_X509_VUI_OK }
195 return NX_X509_VUI_VERIFY_FAIL
196 }
197 let n: *i64 = u2048_alloc()
198 let e_p: *i64 = sys_mmap(16) as *i64
199 let ex: i64 = nx_x509_pubkey_extract_rsa(issuer_buf, issuer_cert, n, e_p)
200 if ex != NX_X509_PUBKEY_RSA_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
201
202 let tbs_ptr: *u8 = buf + cert.tbs_off
203 let sig_ptr: *u8 = buf + cert.sig_off
204 let v: i64 = nx_x509_verify_rsa_pkcs1_sha256(tbs_ptr, cert.tbs_len,
205 sig_ptr, cert.sig_len,
206 n, *e_p)
207 sys_munmap(n as *u8, 256); sys_munmap(e_p as *u8, 16) // S-CLASS certloop leak fix
208 if v == NX_X509_RSA_PKCS1_SHA256_OK { return NX_X509_VUI_OK }
209 return NX_X509_VUI_VERIFY_FAIL
210 // NOTE 2026-05-20: try-2048-then-4096 dispatch removed to
211 // avoid pushing the existing live-CA smoke over the C
212 // bootstrap parser's mconsts[2048] cap. RSA-4096 substrate
213 // is fully shipped (nx_u4096 / nx_x509_pubkey_rsa_4096 /
214 // nx_x509_verify_rsa_pkcs1_sha256_4096 / nx_rsa4096_mod_exp
215 // -- commits 11f30b2d, 68404530, 1eb32bbb, 22056ddb,
216 // 946618bb). Re-integrate here when Task #79 (nx_parse.nx
217 // self-host promotion) raises the const cap. See
218 // [[project-rsa4096-arc-closed-2026-05-20]] memory.
219 }
220
221 if sig_alg == NX_X509_SIG_ALG_RSA_PKCS1_SHA384 {
222 if pub_alg != NX_X509_PUBKEY_ALG_RSA {
223 return NX_X509_VUI_ALG_MISMATCH
224 }
225 // Dispatch by signature length (= issuer modulus bytes): 256=2048,
226 // 512=4096, else (384=3072) k-aware. Reuses shipped mod-exp +
227 // extractors; only the hash differs from the SHA-256 RSA branch.
228 if cert.sig_len == 512 {
229 let n4: *i64 = u4096_alloc()
230 let e4_p: *i64 = sys_mmap(16) as *i64
231 let ex4: i64 = nx_x509_pubkey_extract_rsa_4096(issuer_buf, issuer_cert, n4, e4_p)
232 if ex4 != NX_X509_PUBKEY_RSA_4096_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
233 let tbs4: *u8 = buf + cert.tbs_off
234 let sig4: *u8 = buf + cert.sig_off
235 let v4: i64 = rsa_pkcs1_v1_5_sha384_4096_verify(tbs4, cert.tbs_len,
236 sig4, n4, *e4_p)
237 u4096_free(n4); sys_munmap(e4_p as *u8, 16) // S-CLASS certloop leak fix (RSA-NX_MAGIC_4096 SHA384)
238 if v4 == NX_RSA_PKCS1_V15_384_OK { return NX_X509_VUI_OK }
239 return NX_X509_VUI_VERIFY_FAIL
240 }
241 if cert.sig_len > 256 {
242 // 257..511 bytes => 3072-bit (384) issuer key: k-aware verify.
243 let nk: *i64 = u4096_alloc()
244 let ek_p: *i64 = sys_mmap(16) as *i64
245 let exk: i64 = nx_x509_pubkey_extract_rsa_4096(issuer_buf, issuer_cert, nk, ek_p)
246 if exk != NX_X509_PUBKEY_RSA_4096_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
247 let tbsk: *u8 = buf + cert.tbs_off
248 let sigk: *u8 = buf + cert.sig_off
249 let vk: i64 = rsa_pkcs1_v1_5_sha384_k_verify(tbsk, cert.tbs_len,
250 sigk, cert.sig_len, nk, *ek_p)
251 u4096_free(nk); sys_munmap(ek_p as *u8, 16) // S-CLASS certloop leak fix (RSA-3072k SHA384)
252 if vk == NX_RSA_PKCS1_V15_384_OK { return NX_X509_VUI_OK }
253 return NX_X509_VUI_VERIFY_FAIL
254 }
255 let nr: *i64 = u2048_alloc()
256 let er_p: *i64 = sys_mmap(16) as *i64
257 let exr: i64 = nx_x509_pubkey_extract_rsa(issuer_buf, issuer_cert, nr, er_p)
258 if exr != NX_X509_PUBKEY_RSA_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
259 let tbsr: *u8 = buf + cert.tbs_off
260 let sigr: *u8 = buf + cert.sig_off
261 let vr: i64 = rsa_pkcs1_v1_5_sha384_verify(tbsr, cert.tbs_len,
262 sigr, nr, *er_p)
263 sys_munmap(nr as *u8, 256); sys_munmap(er_p as *u8, 16) // S-CLASS certloop leak fix (RSA-NX_MAGIC_2048 SHA384)
264 if vr == NX_RSA_PKCS1_V15_384_OK { return NX_X509_VUI_OK }
265 return NX_X509_VUI_VERIFY_FAIL
266 }
267
268 if sig_alg == NX_X509_SIG_ALG_ED25519 {
269 if pub_alg != NX_X509_PUBKEY_ALG_ED25519 {
270 return NX_X509_VUI_ALG_MISMATCH
271 }
272 let pub32: *u8 = sys_mmap(32)
273 let ex: i64 = nx_x509_pubkey_extract_ed25519(issuer_buf, issuer_cert, pub32)
274 if ex != NX_X509_PUBKEY_ED_OK { return NX_X509_VUI_PUBKEY_EXTRACT_FAIL }
275
276 // Use shipped x509_verify_signature_ed25519: reads tbs+sig
277 // offsets via cert struct, verifies raw 64-byte sig against
278 // 32-byte signer pubkey.
279 let v: i64 = x509_verify_signature_ed25519(buf, cert, pub32)
280 sys_munmap(pub32 as *u8, 32) // S-CLASS certloop leak fix (Ed25519)
281 if v == NX_X509_VAL_OK { return NX_X509_VUI_OK }
282 return NX_X509_VUI_VERIFY_FAIL
283 }
284
285 return NX_X509_VUI_SIG_ALG_UNSUPPORTED
286}
287
288// Compile-only smoke. Real KAT in nx_x509_verify_under_issuer_test.nx.
289func main() -> i64 {
290 return 0
291}