code wiki / (root) / nx_x509_verify_under_issuer.nx

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}