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}