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}