code wiki / (root) / nx_email_submit.nx

nx_email_submit.nx source

↩ module page · 156 lines · 6314 B

1// nx_email_submit.nx -- EMAIL RUNG R6: authenticated submission over TLS. 2// 3// module: nishi-core.email.submit 4// depends: nishi-core.codec.base64 5// capability: CORE_EMAIL 6// 7// The SEND capstone on the client side: take an authenticated, encrypted 8// path to a submission server (RFC 6409 ports 587 STARTTLS / 465 implicit 9// TLS) and log in before handing off to the R1 MAIL/RCPT/DATA flow. The 10// security-critical sequencing + credential encoding is pure and gated 11// here; the actual TLS handshake is performed by the existing nx_tls13 12// when the state machine emits TLS_UPGRADE (the live wiring lives in a 13// later nx_email_submit_io.nx). 14// 15// AUTH PLAIN (RFC 4616): base64("\0authcid\0passwd"), composing the 16// canonical nx_base64 (not reimplemented). 17// AUTH LOGIN: base64(username) / base64(password) challenge tokens. 18// STARTTLS capability detection in the EHLO reply. 19// Submission state machine: 220 -> EHLO -> STARTTLS -> [TLS] -> EHLO -> 20// AUTH -> READY. CRUX SECURITY PROPERTY: if the server does NOT 21// offer STARTTLS the machine ABORTS rather than sending credentials 22// in cleartext. 23// 24// license_tier: INDEPENDENT_REDERIVE 25// genealogy_id: international-research-sources/ietf/rfc_6409 + rfc_4616 + rfc_3207 26// lineage_id: nishi_email_submit_r6 27// 28// nx_safety_envelope: 29// intended_use: "Submission AUTH encode + STARTTLS sequencing. 30// Composes nx_tls13 for the upgrade (live: _io)." 31// sil_target: SIL3 (credential leak / downgrade) 32// evidence: [auth_plain_roundtrip, starttls_required_before_auth, 33// cleartext_creds_refused, port_select] 34// hazard_register: [bug-tape-STARTTLS-strip-downgrade, bug-tape-cred-cleartext] 35// residual_risk: "Live TLS handshake + post-AUTH MAIL/RCPT/DATA 36// reuse R1; OAUTHBEARER/XOAUTH2 are later." 37// verdict: NOT_YET_EVALUATED 38 39import "nx_base64.nx" 40 41// ---- submission states ---- 42const SUB_INIT: i64 = 0 // await 220 greeting 43const SUB_EHLO: i64 = 1 // EHLO sent, await 250 44const SUB_STARTTLS: i64 = 2 // STARTTLS sent, await 220 45const SUB_EHLO2: i64 = 3 // post-TLS EHLO sent, await 250 46const SUB_AUTH: i64 = 4 // AUTH sent, await 235 47const SUB_READY: i64 = 5 // authenticated; ready for MAIL FROM (R1) 48const SUB_FAIL: i64 = 6 49 50// ---- actions ---- 51const SUB_A_NONE: i64 = 0 52const SUB_A_EHLO: i64 = 1 53const SUB_A_STARTTLS: i64 = 2 54const SUB_A_TLS_UPGRADE: i64 = 3 // do nx_tls13 handshake, then resend EHLO 55const SUB_A_AUTH: i64 = 4 56const SUB_A_READY: i64 = 5 57const SUB_A_ABORT: i64 = 6 58 59func nx_submit_action_name(a: i64) -> *u8 { 60 if a == SUB_A_EHLO { return "EHLO" } 61 if a == SUB_A_STARTTLS { return "STARTTLS" } 62 if a == SUB_A_TLS_UPGRADE { return "TLS_UPGRADE" } 63 if a == SUB_A_AUTH { return "AUTH" } 64 if a == SUB_A_READY { return "READY" } 65 if a == SUB_A_ABORT { return "ABORT" } 66 return "NONE" 67} 68 69func sub_catn(out: *u8, oi: i64, s: *u8, len: i64) -> i64 { 70 var k: i64 = 0 71 while k < len { out[oi] = s[k]; oi = oi + 1; k = k + 1 } 72 return oi 73} 74func sub_cat(out: *u8, oi: i64, s: *u8) -> i64 { 75 var k: i64 = 0 76 while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 } 77 return oi 78} 79func sub_is2xx(code: i64) -> i64 { if code >= 200 && code < 300 { return 1 } return 0 } 80 81// Build "AUTH PLAIN <base64(\0user\0pass)>\r\n". scratch holds the raw 82// SASL PLAIN blob. Returns command length. 83func nx_submit_auth_plain(out: *u8, cap: i64, user: *u8, ulen: i64, pass: *u8, plen: i64, scratch: *u8) -> i64 { 84 scratch[0] = 0 as u8 85 var s: i64 = 1 86 var k: i64 = 0 87 while k < ulen { scratch[s] = user[k]; s = s + 1; k = k + 1 } 88 scratch[s] = 0 as u8; s = s + 1 89 k = 0 90 while k < plen { scratch[s] = pass[k]; s = s + 1; k = k + 1 } 91 var oi: i64 = sub_cat(out, 0, "AUTH PLAIN " as *u8) 92 let bl: i64 = b64_encode(scratch, s, out + oi) 93 oi = oi + bl 94 out[oi] = 13 as u8; out[oi + 1] = 10 as u8 95 return oi + 2 96} 97 98// Build a base64 AUTH LOGIN challenge token: base64(data)\r\n. 99func nx_submit_auth_login_token(out: *u8, cap: i64, data: *u8, dlen: i64) -> i64 { 100 let bl: i64 = b64_encode(data, dlen, out) 101 out[bl] = 13 as u8; out[bl + 1] = 10 as u8 102 return bl + 2 103} 104 105// 1 iff the EHLO response advertises STARTTLS. 106func nx_submit_starttls_avail(resp: *u8, n: i64) -> i64 { 107 let needle: *u8 = "STARTTLS" as *u8 108 var i: i64 = 0 109 while i + 8 <= n { 110 var hit: i64 = 1 111 var k: i64 = 0 112 while k < 8 { if (resp[i + k] & 0xff) != (needle[k] & 0xff) { hit = 0; k = 8 } else { k = k + 1 } } 113 if hit == 1 { return 1 } 114 i = i + 1 115 } 116 return 0 117} 118 119// Submission port: 465 implicit TLS, 587 STARTTLS (RFC 6409 / 8314). 120func nx_submit_port(implicit_tls: i64) -> i64 { 121 if implicit_tls == 1 { return 465 } 122 return 587 123} 124 125// Submission state machine. want_tls=1 demands an encrypted path; 126// starttls_avail says whether the server offered it. Sets *out_action. 127func nx_submit_advance(state: i64, code: i64, want_tls: i64, starttls_avail: i64, out_action: *i64) -> i64 { 128 if state == SUB_INIT { 129 if code == 220 { *out_action = SUB_A_EHLO; return SUB_EHLO } 130 *out_action = SUB_A_ABORT; return SUB_FAIL 131 } 132 if state == SUB_EHLO { 133 if sub_is2xx(code) == 1 { 134 if want_tls == 1 { 135 if starttls_avail == 1 { *out_action = SUB_A_STARTTLS; return SUB_STARTTLS } 136 *out_action = SUB_A_ABORT; return SUB_FAIL // refuse cleartext creds 137 } 138 *out_action = SUB_A_AUTH; return SUB_AUTH 139 } 140 *out_action = SUB_A_ABORT; return SUB_FAIL 141 } 142 if state == SUB_STARTTLS { 143 if code == 220 { *out_action = SUB_A_TLS_UPGRADE; return SUB_EHLO2 } 144 *out_action = SUB_A_ABORT; return SUB_FAIL 145 } 146 if state == SUB_EHLO2 { 147 if sub_is2xx(code) == 1 { *out_action = SUB_A_AUTH; return SUB_AUTH } 148 *out_action = SUB_A_ABORT; return SUB_FAIL 149 } 150 if state == SUB_AUTH { 151 if code == 235 { *out_action = SUB_A_READY; return SUB_READY } 152 *out_action = SUB_A_ABORT; return SUB_FAIL 153 } 154 *out_action = SUB_A_NONE 155 return state 156}