code wiki / (root) / nx_x509_validity.nx

nx_x509_validity.nx source

↩ module page · 196 lines · 8149 B

1// nx_x509_validity.nx -- X.509 Validity period parse + check. 2// 3// Phase 0b §I.2.B of the Nishi TLS 1.3 stack. Composes the 4// X509Cert.validity_off/len (captured by nx_x509.x509_parse) with 5// the shipped parse_utctime / parse_gentime helpers in nx_timefmt 6// to produce notBefore / notAfter as Unix epoch seconds, and 7// gates a now_epoch against that range. 8// 9// X.509 Validity ASN.1 (RFC 5280 §4.1.2.5): 10// 11// Validity ::= SEQUENCE { 12// notBefore Time, 13// notAfter Time 14// } 15// 16// Time ::= CHOICE { 17// utcTime UTCTime, -- ASN.1 tag 0x17, 13 bytes "YYMMDDhhmmssZ" 18// generalTime GeneralizedTime -- ASN.1 tag 0x18, 15 bytes "YYYYMMDDhhmmssZ" 19// } 20// 21// Per RFC 5280, dates with year < 2050 use UTCTime; dates >= 2050 22// must use GeneralizedTime. Web PKI uses UTCTime almost universally 23// today; GeneralizedTime support is required for parsing Let's 24// Encrypt's distant-future "magic" notAfter on long-lived roots. 25// 26// What this primitive does: 27// - x509_validity_get(buf, cert, out_nb_epoch, out_na_epoch) 28// parses the two Time fields out of the validity SEQUENCE body 29// and writes epoch-seconds to caller's out slots. Returns a 30// sealed verdict. 31// 32// - x509_validity_check(buf, cert, now_epoch_secs) 33// calls validity_get internally and decides OK / NOT_YET_VALID / 34// EXPIRED. This is the function the chain-walker will call. 35// 36// - nx_x509_validity_verdict_is_valid(v) sealed-enum gate. 37// 38// What it does NOT do (intentionally split, single-responsibility): 39// - Outer cert signature verify (x509_verify_signature_ed25519) 40// - SAN hostname match (queued §I.2.C) 41// - Chain walk to a trust anchor (queued §I.3) 42// - Clock skew tolerance (caller's responsibility; for TLS clients 43// a typical tolerance of 5 minutes either side is applied at the 44// call site, not buried here). 45// 46// Per Cardinal 12 (defensive at boundaries): all ASN.1 parsing is 47// length-bounded against cert.validity_len; any malformed input 48// returns NX_X509_VALID_BAD_FORMAT rather than reading past the 49// SEQUENCE body. 50// 51// license_tier: INDEPENDENT_REDERIVE 52// genealogy_id: international-research-sources/ietf/rfc_5280 53// lineage_id: nishi_x509_validity_q10 54 55// nx_safety_envelope: 56// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 57// sil_target: SIL1 58// evidence: [bulk_applied_2026-05-19, rfc-5280-validity-check] 59// verdict: NOT_YET_EVALUATED 60 61import "nx_syscalls.nx" 62import "nx_asn1.nx" 63import "nx_x509.nx" 64import "nx_timefmt.nx" 65 66// --- Sealed verdict enum ------------------------------------------- 67 68const NX_X509_VALID_OK: i64 = 1 69const NX_X509_VALID_NOT_YET_VALID: i64 = 2 70const NX_X509_VALID_EXPIRED: i64 = 3 71const NX_X509_VALID_BAD_FORMAT: i64 = 4 72const NX_X509_VALID_BAD_TAG: i64 = 5 73const NX_X509_VALID_BAD_LENGTH: i64 = 6 74const NX_X509_VALID_INVERTED: i64 = 7 // notBefore > notAfter 75const NX_X509_VALID_NO_VALIDITY: i64 = 8 // cert.validity_len == 0 76const NX_X509_VALID_VERDICT_N: i64 = 9 77 78func nx_x509_validity_verdict_is_valid(v: i64) -> i64 { 79 if v < NX_X509_VALID_OK { return 0 } 80 if v >= NX_X509_VALID_VERDICT_N { return 0 } 81 return 1 82} 83 84// --- Single-time parse --------------------------------------------- 85// 86// Read one Time TLV starting at (buf + off). On success writes 87// the epoch-seconds value to *out_epoch, the bytes-consumed count 88// to *out_consumed, and returns NX_X509_VALID_OK. On any 89// malformed input (bad tag, wrong inner length, parse failure) 90// returns a negative-OR-non-OK verdict. 91// 92// Caller invariants: 93// - off < off + (max bytes available to read) must stay within 94// the validity SEQUENCE body. We range-check against the 95// caller-supplied remaining length. 96func x509_read_time_tlv(buf: *u8, off: i64, remaining: i64, 97 out_epoch: *i64, out_consumed: *i64) -> i64 { 98 if remaining < 2 { return NX_X509_VALID_BAD_FORMAT } 99 let tag: i64 = buf[off] & 0xff 100 let inner_len: i64 = buf[off + 1] & 0xff 101 // Time fields use short-form length (single byte) because their 102 // content is always 13 or 15 bytes -- well under 128. 103 if (inner_len & 0x80) != 0 { return NX_X509_VALID_BAD_LENGTH } 104 let total: i64 = 2 + inner_len 105 if total > remaining { return NX_X509_VALID_BAD_LENGTH } 106 if tag == ASN1_UTC_TIME { 107 if inner_len != 13 { return NX_X509_VALID_BAD_LENGTH } 108 let epoch: i64 = parse_utctime(buf + off + 2) 109 if epoch < 0 { return NX_X509_VALID_BAD_FORMAT } 110 *out_epoch = epoch 111 *out_consumed = total 112 return NX_X509_VALID_OK 113 } 114 if tag == ASN1_GENERALIZED { 115 if inner_len != 15 { return NX_X509_VALID_BAD_LENGTH } 116 let epoch: i64 = parse_gentime(buf + off + 2) 117 if epoch < 0 { return NX_X509_VALID_BAD_FORMAT } 118 *out_epoch = epoch 119 *out_consumed = total 120 return NX_X509_VALID_OK 121 } 122 return NX_X509_VALID_BAD_TAG 123} 124 125// --- Public API: parse both Time fields ---------------------------- 126// 127// Walk the validity SEQUENCE body captured by x509_parse and 128// extract notBefore + notAfter as epoch-seconds. Returns 129// NX_X509_VALID_OK on success, or a non-OK verdict (sealed enum). 130// 131// Caller responsibility: 132// - cert must have been populated by x509_parse against buf 133// - out_nb_epoch / out_na_epoch must point to writable i64 slots 134// 135// Inverted range (notBefore > notAfter) is treated as malformed 136// per RFC 5280 §4.1.2.5 ("The validity period for a certificate is 137// the period of time from notBefore through notAfter, inclusive"). 138func x509_validity_get(buf: *u8, cert: *X509Cert, 139 out_nb_epoch: *i64, out_na_epoch: *i64) -> i64 { 140 if cert.validity_len <= 0 { return NX_X509_VALID_NO_VALIDITY } 141 let body_off: i64 = cert.validity_off 142 let body_len: i64 = cert.validity_len 143 144 // First Time: notBefore. 145 let c1_p: *i64 = sys_mmap(16) as *i64 146 let v1: i64 = x509_read_time_tlv(buf, body_off, body_len, 147 out_nb_epoch, c1_p) 148 if v1 != NX_X509_VALID_OK { return v1 } 149 150 // Second Time: notAfter, starting where the first ended. 151 let after_first: i64 = body_off + *c1_p 152 let remaining: i64 = body_len - *c1_p 153 if remaining < 2 { return NX_X509_VALID_BAD_FORMAT } 154 let c2_p: *i64 = sys_mmap(16) as *i64 155 let v2: i64 = x509_read_time_tlv(buf, after_first, remaining, 156 out_na_epoch, c2_p) 157 if v2 != NX_X509_VALID_OK { return v2 } 158 159 // Whole body must be consumed by the two times. Any trailing 160 // bytes mean the cert is malformed (RFC 5280: Validity has 161 // exactly two elements). 162 if (*c1_p + *c2_p) != body_len { return NX_X509_VALID_BAD_FORMAT } 163 164 // Sanity: notBefore must precede notAfter (inclusive equal OK). 165 if *out_nb_epoch > *out_na_epoch { return NX_X509_VALID_INVERTED } 166 167 return NX_X509_VALID_OK 168} 169 170// --- Public API: range-check against now --------------------------- 171// 172// Decide whether the cert is currently valid given a caller-supplied 173// epoch (typically obtained via wall_clock_ns() / 1_000_000_000). 174// 175// Returns: 176// NX_X509_VALID_OK -- now is within [notBefore, notAfter] 177// NX_X509_VALID_NOT_YET_VALID -- now < notBefore (cert "from the future") 178// NX_X509_VALID_EXPIRED -- now > notAfter (cert past sell-by) 179// non-OK verdicts -- propagated from validity_get 180// 181// Caller should treat any non-OK as "do NOT trust this cert". 182func x509_validity_check(buf: *u8, cert: *X509Cert, 183 now_epoch_secs: i64) -> i64 { 184 let nb_p: *i64 = sys_mmap(16) as *i64 185 let na_p: *i64 = sys_mmap(16) as *i64 186 let v: i64 = x509_validity_get(buf, cert, nb_p, na_p) 187 if v != NX_X509_VALID_OK { return v } 188 if now_epoch_secs < *nb_p { return NX_X509_VALID_NOT_YET_VALID } 189 if now_epoch_secs > *na_p { return NX_X509_VALID_EXPIRED } 190 return NX_X509_VALID_OK 191} 192 193// Compile-only smoke. Real KAT in nx_x509_validity_test.nx. 194func main() -> i64 { 195 return 0 196}