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}