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}