code wiki / _hdl_build / nx_smtp_client_gate.nx
nx_smtp_client_gate.nx source
↩ module page · 220 lines · 10410 B
1// nx_smtp_client_gate.nx -- GATE for EMAIL R1 (SMTP client, nx_smtp_client).
2//
3// Drives the REAL nx_smtp_client codec + state machine over scripted
4// transcripts (no network, fully deterministic) and asserts:
5//
6// COMPLETENESS : multiline 250 reply parses to code=250/complete=1;
7// 220 greeting + 354 data-go parse; EHLO/MAIL/RCPT/
8// QUIT/DATA builders emit exact CRLF-framed bytes;
9// dot-stuffing turns a lone "." line into ".." and
10// terminates with <CRLF>.<CRLF>; the state machine
11// walks the full happy path to DONE.
12// NEG-CONTROL : a 550 at RCPT routes the state machine to ABORT/FAIL
13// (no DATA is ever sent to a rejected recipient).
14// TAMPER reply : a continuation line whose code differs from the
15// first line -> CODE_MISMATCH; a non-digit code ->
16// BAD_CODE; a continuation with no final line ->
17// parsed but complete=0 (not treated as a finished
18// reply -- streaming/forgery safe).
19// TAMPER inject: the dot-stuffed body contains the "<CRLF>.<CRLF>"
20// terminator EXACTLY ONCE, at the very end -- a lone
21// "." line cannot prematurely terminate DATA (SMTP
22// injection / truncation defense, RFC 5321 ยง4.5.2).
23//
24// Evidence -> knowledge/status/smtp_client.log
25// (SMTPCLIENTGATE authored=organ ... verdict=GREEN)
26// license_tier: ORIGINAL
27import "nx_smtp_client.nx"
28import "nx_syscalls.nx"
29import "nx_gate_verdict.nx"
30
31const SMTP_LOG: *u8 = "knowledge/status/smtp_client.log"
32
33func 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 }
34func ewn(fd: i64, v: i64) -> i64 {
35 let bb: *u8 = sys_mmap(28); var m: i64 = v
36 if m < 0 { m = 0 - m; sys_write(fd, "-" as *u8, 1) }
37 let t: *u8 = sys_mmap(28); var k: i64 = 0
38 if m == 0 { t[0] = 48; k = 1 }
39 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
40 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
41 sys_write(fd, bb, k); return 0
42}
43
44func slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
45func 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 }
46func find_sub(hay: *u8, hlen: i64, needle: *u8, nlen: i64) -> i64 {
47 var i: i64 = 0
48 while i + nlen <= hlen {
49 var k: i64 = 0
50 var hit: i64 = 1
51 while k < nlen { if hay[i + k] != needle[k] { hit = 0; k = nlen } else { k = k + 1 } }
52 if hit == 1 { return i }
53 i = i + 1
54 }
55 return 0 - 1
56}
57func count_sub(hay: *u8, hlen: i64, needle: *u8, nlen: i64) -> i64 {
58 var c: i64 = 0
59 var i: i64 = 0
60 while i + nlen <= hlen {
61 var k: i64 = 0
62 var hit: i64 = 1
63 while k < nlen { if hay[i + k] != needle[k] { hit = 0; k = nlen } else { k = k + 1 } }
64 if hit == 1 { c = c + 1; i = i + nlen } else { i = i + 1 }
65 }
66 return c
67}
68
69func main() -> i64 {
70 var ok: i64 = 1
71 let cmpl: *i64 = sys_mmap(8) as *i64
72 let cons: *i64 = sys_mmap(8) as *i64
73 let oact: *i64 = sys_mmap(8) as *i64
74 let buf: *u8 = sys_mmap(1024)
75
76 // ---- reply parse: multiline 250 ----
77 let r_ehlo: *u8 = "250-mail.nishi Hello\r\n250-PIPELINING\r\n250 SIZE 1000\r\n" as *u8
78 let le: i64 = slen(r_ehlo)
79 let code_ehlo: i64 = nx_smtp_parse_reply(r_ehlo, le, cmpl, cons)
80 if code_ehlo != 250 { ok = 0 }
81 if *cmpl != 1 { ok = 0 }
82 if *cons != le { ok = 0 }
83
84 // ---- reply parse: single-line greeting + data-go ----
85 let r_greet: *u8 = "220 mail.nishi ESMTP ready\r\n" as *u8
86 let code_greet: i64 = nx_smtp_parse_reply(r_greet, slen(r_greet), cmpl, cons)
87 if code_greet != 220 { ok = 0 }
88 if *cmpl != 1 { ok = 0 }
89 let r_data: *u8 = "354 Start mail input; end with <CRLF>.<CRLF>\r\n" as *u8
90 let code_data: i64 = nx_smtp_parse_reply(r_data, slen(r_data), cmpl, cons)
91 if code_data != 354 { ok = 0 }
92
93 // ---- command builders (exact bytes) ----
94 var builders_ok: i64 = 1
95 let l1: i64 = nx_smtp_build_ehlo(buf, 1024, "relay.nishi" as *u8, 11)
96 if l1 != slen("EHLO relay.nishi\r\n" as *u8) { builders_ok = 0 }
97 if memeq(buf, "EHLO relay.nishi\r\n" as *u8, l1) != 1 { builders_ok = 0 }
98 let l2: i64 = nx_smtp_build_mail_from(buf, 1024, "a@nishi.test" as *u8, 12)
99 if memeq(buf, "MAIL FROM:<a@nishi.test>\r\n" as *u8, l2) != 1 { builders_ok = 0 }
100 let l3: i64 = nx_smtp_build_rcpt_to(buf, 1024, "a@nishi.test" as *u8, 12)
101 if memeq(buf, "RCPT TO:<a@nishi.test>\r\n" as *u8, l3) != 1 { builders_ok = 0 }
102 let l4: i64 = nx_smtp_build_simple(buf, 1024, "QUIT" as *u8)
103 if memeq(buf, "QUIT\r\n" as *u8, l4) != 1 { builders_ok = 0 }
104 let l5: i64 = nx_smtp_build_simple(buf, 1024, "DATA" as *u8)
105 if memeq(buf, "DATA\r\n" as *u8, l5) != 1 { builders_ok = 0 }
106 if builders_ok != 1 { ok = 0 }
107
108 // ---- dot-stuffing ----
109 let ds_in: *u8 = "A\r\n.\r\n.B\r\n" as *u8
110 let ds_out: *u8 = sys_mmap(256)
111 let ds_len: i64 = nx_smtp_dot_stuff(ds_in, slen(ds_in), ds_out, 256)
112 let ds_exp: *u8 = "A\r\n..\r\n..B\r\n.\r\n" as *u8
113 var dotstuff_ok: i64 = 1
114 if ds_len != slen(ds_exp) { dotstuff_ok = 0 }
115 if memeq(ds_out, ds_exp, ds_len) != 1 { dotstuff_ok = 0 }
116 if dotstuff_ok != 1 { ok = 0 }
117 // injection defense: terminator <CRLF>.<CRLF> appears once, at the end
118 let term: *u8 = "\r\n.\r\n" as *u8
119 let term_cnt: i64 = count_sub(ds_out, ds_len, term, 5)
120 let term_pos: i64 = find_sub(ds_out, ds_len, term, 5)
121 var term_only_end: i64 = 0
122 if term_cnt == 1 && term_pos == ds_len - 5 { term_only_end = 1 } else { ok = 0 }
123
124 // ---- state machine: happy path to DONE ----
125 let codes_h: *i64 = sys_mmap(8 * 8) as *i64
126 codes_h[0] = 220; codes_h[1] = 250; codes_h[2] = 250; codes_h[3] = 250
127 codes_h[4] = 354; codes_h[5] = 250; codes_h[6] = 221
128 let acts: *i64 = sys_mmap(8 * 8) as *i64
129 var st: i64 = SMTP_S_INIT
130 var na: i64 = 0
131 var ci: i64 = 0
132 while ci < 7 && st != SMTP_S_DONE && st != SMTP_S_FAIL {
133 st = nx_smtp_advance(st, codes_h[ci], oact)
134 acts[na] = *oact
135 na = na + 1
136 ci = ci + 1
137 }
138 var happy_ok: i64 = 1
139 if na != 7 { happy_ok = 0 }
140 if st != SMTP_S_DONE { happy_ok = 0 }
141 if na == 7 {
142 if acts[0] != SMTP_A_SEND_EHLO { happy_ok = 0 }
143 if acts[1] != SMTP_A_SEND_MAIL { happy_ok = 0 }
144 if acts[2] != SMTP_A_SEND_RCPT { happy_ok = 0 }
145 if acts[3] != SMTP_A_SEND_DATA { happy_ok = 0 }
146 if acts[4] != SMTP_A_SEND_BODY { happy_ok = 0 }
147 if acts[5] != SMTP_A_SEND_QUIT { happy_ok = 0 }
148 if acts[6] != SMTP_A_DONE { happy_ok = 0 }
149 }
150 if happy_ok != 1 { ok = 0 }
151
152 // ---- NEG-CONTROL: 550 at RCPT -> ABORT/FAIL ----
153 let codes_n: *i64 = sys_mmap(8 * 8) as *i64
154 codes_n[0] = 220; codes_n[1] = 250; codes_n[2] = 250; codes_n[3] = 550
155 var st2: i64 = SMTP_S_INIT
156 var na2: i64 = 0
157 var last_act: i64 = SMTP_A_NONE
158 var cj: i64 = 0
159 while cj < 4 && st2 != SMTP_S_DONE && st2 != SMTP_S_FAIL {
160 st2 = nx_smtp_advance(st2, codes_n[cj], oact)
161 last_act = *oact
162 na2 = na2 + 1
163 cj = cj + 1
164 }
165 var neg_ok: i64 = 0
166 if last_act == SMTP_A_ABORT && st2 == SMTP_S_FAIL { neg_ok = 1 } else { ok = 0 }
167
168 // ---- TAMPER: code mismatch across continuation lines ----
169 let r_mis: *u8 = "250-foo\r\n251 bar\r\n" as *u8
170 let cm: i64 = nx_smtp_parse_reply(r_mis, slen(r_mis), cmpl, cons)
171 var tamper_mismatch: i64 = 0
172 if cm == (0 - NX_SMTP_CODE_MISMATCH) { tamper_mismatch = 1 } else { ok = 0 }
173
174 // ---- TAMPER: non-digit code ----
175 let r_bad: *u8 = "2X0 nope\r\n" as *u8
176 let cb: i64 = nx_smtp_parse_reply(r_bad, slen(r_bad), cmpl, cons)
177 var tamper_badcode: i64 = 0
178 if cb == (0 - NX_SMTP_BAD_CODE) { tamper_badcode = 1 } else { ok = 0 }
179
180 // ---- TAMPER: truncated continuation (no final line) -> complete=0 ----
181 let r_trunc: *u8 = "220-greeting line one\r\n" as *u8
182 let ct: i64 = nx_smtp_parse_reply(r_trunc, slen(r_trunc), cmpl, cons)
183 var tamper_trunc: i64 = 0
184 if ct == 220 && *cmpl == 0 { tamper_trunc = 1 } else { ok = 0 }
185
186 var fd: i64 = 1
187 while fd >= 1 {
188 ew(fd, "SMTPCLIENTGATE authored=organ rfc=5321 reply_multiline_code=" as *u8); ewn(fd, code_ehlo)
189 ew(fd, " reply_complete=1 greeting=" as *u8); ewn(fd, code_greet)
190 ew(fd, " datago=" as *u8); ewn(fd, code_data)
191 ew(fd, " builders_ok=" as *u8); ewn(fd, builders_ok)
192 ew(fd, " happy_actions=" as *u8); ewn(fd, na)
193 ew(fd, " happy_final=" as *u8); if st == SMTP_S_DONE { ew(fd, "DONE" as *u8) } else { ew(fd, "NOT_DONE" as *u8) }
194 ew(fd, " neg550_action=" as *u8); ew(fd, nx_smtp_action_name(last_act))
195 ew(fd, " neg_final=" as *u8); if st2 == SMTP_S_FAIL { ew(fd, "FAIL" as *u8) } else { ew(fd, "?" as *u8) }
196 ew(fd, " dotstuff_len=" as *u8); ewn(fd, ds_len)
197 ew(fd, " dotstuff_ok=" as *u8); ewn(fd, dotstuff_ok)
198 ew(fd, " term_only_at_end=" as *u8); ewn(fd, term_only_end)
199 ew(fd, " tamper_mismatch=" as *u8); if tamper_mismatch == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) }
200 ew(fd, " tamper_badcode=" as *u8); if tamper_badcode == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) }
201 ew(fd, " tamper_truncated=" as *u8); if tamper_trunc == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) }
202 if ok == 1 { ew(fd, " verdict=GREEN\n" as *u8) } else { ew(fd, " verdict=RED\n" as *u8) }
203 if fd == 1 {
204 let lf: i64 = sys_openat_append(SMTP_LOG, 420)
205 if lf >= 1 { fd = lf } else { fd = 0 }
206 } else {
207 sys_close(fd); fd = 0
208 }
209 }
210
211 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
212 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
213 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
214 let ctr__dry: *i64 = gv_ctr()
215 ctr__dry[0] = ok
216 ctr__dry[1] = 1
217 let rc__dry: i64 = gv_verdict("SMTP-CLIENT-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8)
218 sys_exit(rc__dry)
219 return rc__dry
220}