code wiki / (root) / nx_email_send.nx

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}