nx_cert_monitor.nx source
↩ module page · 88 lines · 5599 B
1// nx_cert_monitor.nx -- CERT-EXPIRY MONITOR (closes hosting_research gap #3 "cert-expiry-monitor", 3/0
2// CONFIRMED: best-in-class hosts auto-renew + monitor SSL expiry; Nishi certs are hardcoded, no expiry
3// alert -- "a silent cert expiry = a self-inflicted outage"). Pure COMPOSITION (rule 15 DRY): reuses the
4// shipped X.509 stack -- x509_parse (nx_x509) captures the validity field; x509_validity_get
5// (nx_x509_validity) returns notBefore/notAfter as Unix epoch seconds via parse_utctime/parse_gentime;
6// sys_now_realtime_sec is `now`. We just compute days-until-expiry and turn it into an actionable verdict.
7//
8// module: nishi-core.supervision.cert_monitor capability: DAEMON_ROBUSTNESS / observability
9import "nx_x509.nx"
10import "nx_x509_validity.nx"
11const CM_MAGIC_86400: i64 = 86400
12const CM_MAGIC_65536: i64 = 65536
13const CM_MAGIC_1024: i64 = 1024
14
15const CM_OK: i64 = 0 // > warn_days remain
16const CM_WARN: i64 = 1 // 0..warn_days remain -> RENEW NOW (the alert best-in-class fires, Nishi never did)
17const CM_EXPIRED: i64 = 2 // notAfter is in the past -> the site is serving an expired cert (or about to fail TLS)
18const CM_NOCERT: i64 = 3 // could not read/parse the cert
19const CM_WARN_DAYS: i64 = 21 // LE certs live 90d; alert ~3 weeks before expiry (well inside the renew window)
20
21func cm_days_until(na_epoch: i64, now_epoch: i64) -> i64 { return (na_epoch - now_epoch) / CM_MAGIC_86400 }
22func cm_verdict(na_epoch: i64, now_epoch: i64, warn_days: i64) -> i64 {
23 let d: i64 = cm_days_until(na_epoch, now_epoch)
24 if d < 0 { return CM_EXPIRED }
25 if d < warn_days { return CM_WARN }
26 return CM_OK
27}
28func cm_name(v: i64) -> *u8 {
29 if v == CM_OK { return "OK" as *u8 }
30 if v == CM_WARN { return "WARN (expiring soon -- RENEW)" as *u8 }
31 if v == CM_EXPIRED { return "EXPIRED (self-inflicted outage)" as *u8 }
32 return "NOCERT (could not read/parse)" as *u8
33}
34
35func cm_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
36func cm_n(v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m; sys_write(1,"-" as *u8,1)} var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} let b: *u8=sys_mmap(28); var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0 }
37
38// read a DER cert file -> buf (returns bytes, or <=0). The LE cert lives on the NAS at
39// /volume1/homes/elderwesto/nishihost/certs/le_fullchain.der; run there for the live check.
40func cm_read(path: *u8, buf: *u8, cap: i64) -> i64 {
41 let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 }
42 var off: i64=0; var go: i64=1
43 while go==1 { if off>=cap {go=0} else { let r: i64=sys_read(fd, ((buf as i64)+off) as *u8, cap-off); if r<=0 {go=0} else {off=off+r} } }
44 sys_close(fd); return off
45}
46
47// NON-PRINTING core: parse a cert file -> verdict, writing days-until to *out_days and notAfter epoch to
48// *out_na. Returns CM_OK/WARN/EXPIRED/NOCERT. This is the seam the status dashboard consumes (rule 15 DRY:
49// one cert-status fact, many readers -- the monitor's main + nx_status_page).
50func cm_status(path: *u8, out_days: *i64, out_na: *i64) -> i64 {
51 out_days[0] = 0; out_na[0] = 0
52 let buf: *u8 = sys_mmap(CM_MAGIC_65536); let n: i64 = cm_read(path, buf, CM_MAGIC_65536)
53 if n <= 0 { return CM_NOCERT }
54 let cert: *X509Cert = sys_mmap(CM_MAGIC_1024) as *X509Cert
55 if x509_parse(buf, n, cert) != 0 { return CM_NOCERT }
56 let nb: *i64 = sys_mmap(16) as *i64; let na: *i64 = sys_mmap(16) as *i64
57 if x509_validity_get(buf, cert, nb, na) != NX_X509_VALID_OK { return CM_NOCERT }
58 let now: i64 = sys_now_realtime_sec()
59 out_na[0] = na[0]; out_days[0] = cm_days_until(na[0], now)
60 return cm_verdict(na[0], now, CM_WARN_DAYS)
61}
62
63// monitor one cert file: parse -> notAfter epoch -> days-until -> verdict + print. Returns the verdict.
64func cm_check_file(path: *u8) -> i64 {
65 let d: *i64 = sys_mmap(16) as *i64; let na: *i64 = sys_mmap(16) as *i64
66 let v: i64 = cm_status(path, d, na)
67 cm_p(" cert " as *u8); cm_p(path); cm_p(" -> " as *u8)
68 if v == CM_NOCERT { cm_p("NOCERT (not found / unparseable here)\n" as *u8); return CM_NOCERT }
69 cm_p(cm_name(v) as *u8); cm_p(" days_until=" as *u8); cm_n(d[0]); cm_p(" (notAfter_epoch=" as *u8); cm_n(na[0]); cm_p(")\n" as *u8)
70 return v
71}
72
73func main() -> i64 {
74 cm_p("=== NISHI CERT-EXPIRY MONITOR (per-domain SNI certs) ===\n" as *u8)
75 // BOTH per-domain wildcard certs nx_sites_daemon loads by SNI -- local repo copies (if staged here) and
76 // the LIVE NAS paths (resolve only when this is run ON the NAS). The legacy single le_fullchain.der was
77 // pre-SNI; the live certs are now per-domain (le_nishifamily_* / le_andelinwest_*).
78 cm_p(" -- nishifamily.com --\n" as *u8)
79 cm_check_file("/mnt/c/Users/elder/nishi-core/nxc2/certs/le_nishifamily_fullchain.der" as *u8)
80 cm_check_file("/volume1/homes/elderwesto/nishihost/certs/le_nishifamily_fullchain.der" as *u8)
81 cm_p(" -- andelinwest.com --\n" as *u8)
82 cm_check_file("/mnt/c/Users/elder/nishi-core/nxc2/certs/le_andelinwest_fullchain.der" as *u8)
83 cm_check_file("/volume1/homes/elderwesto/nishihost/certs/le_andelinwest_fullchain.der" as *u8)
84 // NOTE: this is the FILE-based view (authoritative only on the NAS). For LIVE served-cert monitoring +
85 // unattended renewal from anywhere, run nx_cert_autorenew (it TLS-probes each SNI cert, then renews any
86 // inside the 21d warn window). The data-driven domain list lives in knowledge/hosting/cert_domains.conf.
87 sys_exit(0); return 0
88}