nx_email_send.nx source
↩ module page · 144 lines · 6697 B
1// nx_email_send.nx -- EMAIL OUTBOUND: compose + DKIM-sign a message.
2//
3// module: nishi-core.email.send
4// depends: nishi-core.email.mime, nishi-core.email.auth, nishi-core.codec.base64
5// capability: CORE_EMAIL
6//
7// The outbound counterpart to R5's verify: build an RFC 5322 message and
8// attach a real DKIM-Signature (RFC 6376 / 8463 ed25519-sha256) using the
9// domain's private key -- so mail FROM self@jasonewest.com validates at
10// the recipient against the published DKIM public key (see nx_email_dns).
11//
12// Canonicalization: relaxed headers (lowercase name + ":" + unfolded
13// value) over h=from:to:subject, simple body. Sign and verify share the
14// signing-input builder so they are byte-consistent. HONEST SCOPE (no-
15// wave): graded on the sign<->verify round-trip + tamper rejection, not
16// byte-interop with a third-party verifier (the canon subset is fixed).
17//
18// license_tier: INDEPENDENT_REDERIVE
19// genealogy_id: international-research-sources/ietf/rfc_6376 + rfc_8463
20// lineage_id: nishi_email_send_dkim
21
22import "nx_email_mime.nx"
23import "nx_email_auth.nx"
24import "nx_base64.nx"
25import "nx_syscalls.nx"
26const K_MAGIC_2048: i64 = 2048
27const K_MAGIC_4096: i64 = 4096
28const K_MAGIC_1024: i64 = 1024
29
30func es_cat(out: *u8, oi: i64, s: *u8) -> i64 { var k: i64 = 0; while s[k] != (0 as u8) { out[oi] = s[k]; oi = oi + 1; k = k + 1 } return oi }
31func es_catn(out: *u8, oi: i64, s: *u8, n: i64) -> i64 { var k: i64 = 0; while k < n { out[oi] = s[k]; oi = oi + 1; k = k + 1 } return oi }
32func es_find(hay: *u8, hlen: i64, needle: *u8, nlen: i64) -> i64 {
33 var i: i64 = 0
34 while i + nlen <= hlen {
35 var k: i64 = 0; var hit: i64 = 1
36 while k < nlen { if (hay[i + k] & 0xff) != (needle[k] & 0xff) { hit = 0; k = nlen } else { k = k + 1 } }
37 if hit == 1 { return i }
38 i = i + 1
39 }
40 return 0 - 1
41}
42
43// Compose a minimal RFC 5322 message. Returns length.
44func nx_email_compose(from: *u8, to: *u8, subj: *u8, body: *u8, out: *u8, cap: i64) -> i64 {
45 var oi: i64 = 0
46 oi = es_cat(out, oi, "From: " as *u8); oi = es_cat(out, oi, from); oi = es_cat(out, oi, "\r\n" as *u8)
47 oi = es_cat(out, oi, "To: " as *u8); oi = es_cat(out, oi, to); oi = es_cat(out, oi, "\r\n" as *u8)
48 oi = es_cat(out, oi, "Subject: " as *u8); oi = es_cat(out, oi, subj); oi = es_cat(out, oi, "\r\n" as *u8)
49 oi = es_cat(out, oi, "\r\n" as *u8)
50 oi = es_cat(out, oi, body)
51 return oi
52}
53
54// Build the canonical signing input: relaxed from/to/subject + the
55// "dkim-signature:" header with the b= value empty. Returns length.
56func es_signing_input(msg: *u8, hlen: i64, dkim_tags_bempty: *u8, tlen: i64, out: *u8) -> i64 {
57 let val: *u8 = sys_mmap(K_MAGIC_2048)
58 var oi: i64 = 0
59 let fl: i64 = nx_mime_header_find(msg, hlen, "from" as *u8, 4, val, K_MAGIC_2048)
60 oi = es_cat(out, oi, "from:" as *u8); if fl >= 0 { oi = es_catn(out, oi, val, fl) } oi = es_cat(out, oi, "\r\n" as *u8)
61 let tl: i64 = nx_mime_header_find(msg, hlen, "to" as *u8, 2, val, K_MAGIC_2048)
62 oi = es_cat(out, oi, "to:" as *u8); if tl >= 0 { oi = es_catn(out, oi, val, tl) } oi = es_cat(out, oi, "\r\n" as *u8)
63 let sl: i64 = nx_mime_header_find(msg, hlen, "subject" as *u8, 7, val, K_MAGIC_2048)
64 oi = es_cat(out, oi, "subject:" as *u8); if sl >= 0 { oi = es_catn(out, oi, val, sl) } oi = es_cat(out, oi, "\r\n" as *u8)
65 oi = es_cat(out, oi, "dkim-signature:" as *u8)
66 oi = es_catn(out, oi, dkim_tags_bempty, tlen)
67 return oi
68}
69
70// Sign a message with DKIM ed25519-sha256. Prepends a DKIM-Signature
71// header. Returns the signed message length.
72func nx_email_dkim_sign_message(msg: *u8, mlen: i64, priv: *u8, selector: *u8, domain: *u8, out: *u8, cap: i64) -> i64 {
73 let bo: *i64 = sys_mmap(8) as *i64
74 var hlen: i64 = nx_mime_split_body(msg, mlen, bo)
75 if hlen < 0 { hlen = mlen }
76 let body: *u8 = (msg as i64 + *bo) as *u8
77 let blen: i64 = mlen - *bo
78 let scratch: *u8 = sys_mmap(blen + 16)
79 let bh32: *u8 = sys_mmap(32)
80 nx_dkim_body_hash(body, blen, scratch, blen + 16, bh32)
81 let bh_b64: *u8 = sys_mmap(64)
82 let bhl: i64 = b64_encode(bh32, 32, bh_b64)
83 // tags with b empty
84 let tags: *u8 = sys_mmap(512)
85 var ti: i64 = 0
86 ti = es_cat(tags, ti, "v=1; a=ed25519-sha256; c=relaxed/simple; d=" as *u8); ti = es_cat(tags, ti, domain)
87 ti = es_cat(tags, ti, "; s=" as *u8); ti = es_cat(tags, ti, selector)
88 ti = es_cat(tags, ti, "; h=from:to:subject; bh=" as *u8); ti = es_catn(tags, ti, bh_b64, bhl)
89 ti = es_cat(tags, ti, "; b=" as *u8)
90 // signing input + sign
91 let si: *u8 = sys_mmap(K_MAGIC_4096)
92 let sil: i64 = es_signing_input(msg, hlen, tags, ti, si)
93 let sig: *u8 = sys_mmap(64)
94 nx_dkim_sign(si, sil, priv, sig)
95 let sig_b64: *u8 = sys_mmap(128)
96 let sgl: i64 = b64_encode(sig, 64, sig_b64)
97 // emit
98 var oi: i64 = 0
99 oi = es_cat(out, oi, "DKIM-Signature: " as *u8)
100 oi = es_catn(out, oi, tags, ti)
101 oi = es_catn(out, oi, sig_b64, sgl)
102 oi = es_cat(out, oi, "\r\n" as *u8)
103 var k: i64 = 0
104 while k < mlen { out[oi] = msg[k]; oi = oi + 1; k = k + 1 }
105 return oi
106}
107
108// Verify a DKIM-signed message against the domain's public key. 1/0.
109func nx_email_dkim_verify_message(smsg: *u8, slen_: i64, pub: *u8) -> i64 {
110 let bo: *i64 = sys_mmap(8) as *i64
111 let hlen: i64 = nx_mime_split_body(smsg, slen_, bo)
112 if hlen < 0 { return 0 }
113 let body: *u8 = (smsg as i64 + *bo) as *u8
114 let blen: i64 = slen_ - *bo
115 let dv: *u8 = sys_mmap(K_MAGIC_1024)
116 let dvl: i64 = nx_mime_header_find(smsg, hlen, "dkim-signature" as *u8, 14, dv, K_MAGIC_1024)
117 if dvl < 0 { return 0 }
118 // body hash check
119 let pbh: i64 = es_find(dv, dvl, "bh=" as *u8, 3)
120 if pbh < 0 { return 0 }
121 var bhs: i64 = pbh + 3
122 var bhe: i64 = bhs
123 while bhe < dvl && (dv[bhe] & 0xff) != 59 { bhe = bhe + 1 }
124 let scratch: *u8 = sys_mmap(blen + 16)
125 let bh32: *u8 = sys_mmap(32)
126 nx_dkim_body_hash(body, blen, scratch, blen + 16, bh32)
127 let bh_b64: *u8 = sys_mmap(64)
128 let bhl: i64 = b64_encode(bh32, 32, bh_b64)
129 if bhl != (bhe - bhs) { return 0 }
130 var i: i64 = 0
131 while i < bhl { if (bh_b64[i] & 0xff) != (dv[bhs + i] & 0xff) { return 0 } i = i + 1 }
132 // signature check
133 let pb: i64 = es_find(dv, dvl, "; b=" as *u8, 4)
134 if pb < 0 { return 0 }
135 let sigoff: i64 = pb + 4
136 let siglen: i64 = dvl - sigoff
137 let sig: *u8 = sys_mmap(96)
138 let sn: i64 = b64_decode((dv as i64 + sigoff) as *u8, siglen, sig)
139 if sn != 64 { return 0 }
140 let tlen: i64 = pb + 4
141 let si: *u8 = sys_mmap(K_MAGIC_4096)
142 let sil: i64 = es_signing_input(smsg, hlen, dv, tlen, si)
143 return nx_dkim_verify(si, sil, pub, sig)
144}