code wiki / (root) / nx_cert_monitor.nx

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}