code wiki / _hdl_build / nx_email_auth_gate.nx

nx_email_auth_gate.nx source

↩ module page · 128 lines · 6464 B

1// nx_email_auth_gate.nx -- GATE for EMAIL R5 (SPF/DKIM/DMARC, nx_email_auth). 2// 3// COMPLETENESS : DKIM body hash is identical across bodies that differ 4// only in trailing blank lines (simple canon); a DKIM 5// ed25519-sha256 header signature round-trips (sign -> 6// verify=1); SPF ip4/cidr + exact + all evaluate; DMARC 7// p= policy parses. 8// NEG-CONTROL : a non-SPF string -> NONE; a non-DMARC string -> norecord. 9// TAMPER : a flipped body byte changes bh= (hashes differ); a 10// flipped signed header fails the signature (forgery 11// rejected); DMARC "sp=reject; p=quarantine" returns 12// QUARANTINE -- the boundary-anchored parser does NOT 13// mistake the subdomain-policy "sp=" for "p=". 14// 15// Evidence -> knowledge/status/email_auth.log 16// (AUTHGATE authored=organ ... verdict=GREEN) 17// license_tier: ORIGINAL 18import "nx_email_auth.nx" 19import "nx_syscalls.nx" 20import "nx_gate_verdict.nx" 21 22const AUTH_LOG: *u8 = "knowledge/status/email_auth.log" 23 24func ew(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 25func epf(fd: i64, label: *u8, pass: i64) -> i64 { 26 ew(fd, label); if pass == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } return 0 27} 28func slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 29func memeq(a: *u8, b: *u8, len: i64) -> i64 { var i: i64 = 0; while i < len { if a[i] != b[i] { return 0 } i = i + 1 } return 1 } 30func mkip(a: i64, b: i64, c: i64, d: i64) -> i64 { return (a << 24) | (b << 16) | (c << 8) | d } 31 32func main() -> i64 { 33 var ok: i64 = 1 34 let scratch: *u8 = sys_mmap(1024) 35 let da: *u8 = sys_mmap(32) 36 let db: *u8 = sys_mmap(32) 37 let dt: *u8 = sys_mmap(32) 38 39 // ---- DKIM body hash: canon idempotent over trailing blank lines ---- 40 let body_a: *u8 = "Hello DKIM\r\nLine two\r\n\r\n\r\n" as *u8 41 let body_b: *u8 = "Hello DKIM\r\nLine two\r\n" as *u8 42 let body_t: *u8 = "Hello DKIN\r\nLine two\r\n" as *u8 43 nx_dkim_body_hash(body_a, slen(body_a), scratch, 1024, da) 44 nx_dkim_body_hash(body_b, slen(body_b), scratch, 1024, db) 45 nx_dkim_body_hash(body_t, slen(body_t), scratch, 1024, dt) 46 var bodyhash_canon: i64 = 0 47 if memeq(da, db, 32) == 1 { bodyhash_canon = 1 } else { ok = 0 } 48 var body_tamper: i64 = 0 49 if memeq(da, dt, 32) == 0 { body_tamper = 1 } else { ok = 0 } 50 51 // ---- DKIM ed25519-sha256 header signature round-trip + tamper ---- 52 let priv: *u8 = sys_mmap(32) 53 var i: i64 = 0 54 while i < 32 { priv[i] = i as u8; i = i + 1 } 55 let pub: *u8 = sys_mmap(32) 56 ed25519_pub_from_priv(priv, pub) 57 let hdrs: *u8 = "from:a@nishi.test\r\nsubject:Test Msg\r\n" as *u8 58 let hdrs_t: *u8 = "from:a@nishi.test\r\nsubject:Test MsX\r\n" as *u8 59 let sig: *u8 = sys_mmap(64) 60 nx_dkim_sign(hdrs, slen(hdrs), priv, sig) 61 var dkim_verify: i64 = nx_dkim_verify(hdrs, slen(hdrs), pub, sig) 62 if dkim_verify != 1 { ok = 0 } 63 var dkim_tamper: i64 = 0 64 if nx_dkim_verify(hdrs_t, slen(hdrs_t), pub, sig) == 0 { dkim_tamper = 1 } else { ok = 0 } 65 66 // ---- SPF ---- 67 let rec1: *u8 = "v=spf1 ip4:192.168.1.0/24 ip4:10.0.0.5 -all" as *u8 68 let r1: i64 = slen(rec1) 69 var spf_cidr: i64 = 0 70 if nx_spf_eval(rec1, r1, mkip(192, 168, 1, 50)) == NX_SPF_PASS { spf_cidr = 1 } else { ok = 0 } 71 var spf_exact: i64 = 0 72 if nx_spf_eval(rec1, r1, mkip(10, 0, 0, 5)) == NX_SPF_PASS { spf_exact = 1 } else { ok = 0 } 73 var spf_fail: i64 = 0 74 if nx_spf_eval(rec1, r1, mkip(8, 8, 8, 8)) == NX_SPF_FAIL { spf_fail = 1 } else { ok = 0 } 75 let rec2: *u8 = "v=spf1 ip4:1.2.3.4 ~all" as *u8 76 var spf_soft: i64 = 0 77 if nx_spf_eval(rec2, slen(rec2), mkip(9, 9, 9, 9)) == NX_SPF_SOFTFAIL { spf_soft = 1 } else { ok = 0 } 78 let rec3: *u8 = "hello not spf" as *u8 79 var spf_none: i64 = 0 80 if nx_spf_eval(rec3, slen(rec3), mkip(1, 1, 1, 1)) == NX_SPF_NONE { spf_none = 1 } else { ok = 0 } 81 82 // ---- DMARC ---- 83 let dm1: *u8 = "v=DMARC1; p=reject; rua=mailto:x@y.test" as *u8 84 var dmarc_reject: i64 = 0 85 if nx_dmarc_policy(dm1, slen(dm1)) == NX_DMARC_REJECT { dmarc_reject = 1 } else { ok = 0 } 86 let dm2: *u8 = "v=DMARC1; sp=reject; p=quarantine" as *u8 87 var dmarc_boundary: i64 = 0 88 if nx_dmarc_policy(dm2, slen(dm2)) == NX_DMARC_QUARANTINE { dmarc_boundary = 1 } else { ok = 0 } 89 let dm3: *u8 = "v=DMARC1; p=none" as *u8 90 var dmarc_none: i64 = 0 91 if nx_dmarc_policy(dm3, slen(dm3)) == NX_DMARC_NONE { dmarc_none = 1 } else { ok = 0 } 92 let dm4: *u8 = "v=spf1 ip4:1.2.3.4" as *u8 93 var dmarc_norec: i64 = 0 94 if nx_dmarc_policy(dm4, slen(dm4)) == NX_DMARC_NOREC { dmarc_norec = 1 } else { ok = 0 } 95 96 var fd: i64 = 1 97 while fd >= 1 { 98 ew(fd, "AUTHGATE authored=organ rfc=6376+8463+7208+7489 " as *u8) 99 epf(fd, "dkim_bodyhash_canon=" as *u8, bodyhash_canon) 100 epf(fd, " dkim_body_tamper=" as *u8, body_tamper) 101 epf(fd, " dkim_sign_verify=" as *u8, dkim_verify) 102 epf(fd, " dkim_hdr_tamper=" as *u8, dkim_tamper) 103 epf(fd, " spf_cidr=" as *u8, spf_cidr) 104 epf(fd, " spf_exact=" as *u8, spf_exact) 105 epf(fd, " spf_fail_all=" as *u8, spf_fail) 106 epf(fd, " spf_softfail=" as *u8, spf_soft) 107 epf(fd, " spf_none_negctrl=" as *u8, spf_none) 108 epf(fd, " dmarc_reject=" as *u8, dmarc_reject) 109 epf(fd, " dmarc_boundary_sp=" as *u8, dmarc_boundary) 110 epf(fd, " dmarc_none=" as *u8, dmarc_none) 111 epf(fd, " dmarc_norec_negctrl=" as *u8, dmarc_norec) 112 if ok == 1 { ew(fd, " verdict=GREEN\n" as *u8) } else { ew(fd, " verdict=RED\n" as *u8) } 113 if fd == 1 { 114 let lf: i64 = sys_openat_append(AUTH_LOG, 420) 115 if lf >= 1 { fd = lf } else { fd = 0 } 116 } else { sys_close(fd); fd = 0 } 117 } 118 119 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 120 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 121 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 122 let ctr__dry: *i64 = gv_ctr() 123 ctr__dry[0] = ok 124 ctr__dry[1] = 1 125 let rc__dry: i64 = gv_verdict("EMAIL-AUTH-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 126 sys_exit(rc__dry) 127 return rc__dry 128}