├── .gitignore ├── Makefile ├── LICENSE.md ├── dane.c ├── pkey_hash.c ├── README.md ├── starttls_client.c └── mxclient.c /.gitignore: -------------------------------------------------------------------------------- 1 | mxclient 2 | *.o 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | srcdir = . 2 | prefix = /usr/local 3 | exec_prefix = $(prefix) 4 | bindir = $(exec_prefix)/bin 5 | 6 | CFLAGS = -O2 -Wall 7 | LDFLAGS = 8 | LIBS = -lbearssl -lpthread -lresolv 9 | INSTALL = install 10 | 11 | -include config.mak 12 | 13 | SRCS = $(sort $(wildcard $(srcdir)/*.c)) 14 | OBJS = $(SRCS:$(srcdir)/%.c=%.o) 15 | 16 | all: mxclient 17 | 18 | clean: 19 | rm -f mxclient *.o 20 | 21 | install: $(DESTDIR)$(bindir)/mxclient 22 | 23 | mxclient: $(OBJS) 24 | $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS) 25 | 26 | %.o: $(srcdir)/%.c 27 | $(CC) $(CFLAGS) -c -o $@ $< 28 | 29 | $(DESTDIR)$(bindir)/mxclient: mxclient 30 | $(INSTALL) mxclient $@ 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Rich Felker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /dane.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int check_tlsa(const unsigned char *pkey_sha256, const unsigned char *pkey_sha512, 6 | const unsigned char *cert_sha256, const unsigned char *cert_sha512, 7 | int is_ee, const unsigned char *tlsa, size_t tlsa_len) 8 | { 9 | if (!tlsa_len) return 0; 10 | 11 | ns_msg msg; 12 | if (ns_initparse(tlsa, tlsa_len, &msg) < 0) 13 | return -1; 14 | ns_rr rr; 15 | for (int i=0; !ns_parserr(&msg, ns_s_an, i, &rr); i++) { 16 | if (ns_rr_type(rr) != 52) continue; 17 | if (ns_rr_rdlen(rr) < 4) return -1; 18 | const unsigned char *pinning = ns_rr_rdata(rr); 19 | 20 | if (!is_ee && (pinning[0] == 3 || pinning[0] == 1)) 21 | continue; // DANE-EE pinnings can't be used for non-EE certs 22 | if (is_ee && (pinning[0] != 3 && pinning[0] != 1)) 23 | continue; // Only DANE-EE pinnings can be used for EE certs 24 | 25 | unsigned char rewritten_cad_buf[32]; 26 | const unsigned char *cad = pinning+3; 27 | size_t cad_len = ns_rr_rdlen(rr)-3; 28 | 29 | int match_type = pinning[2]; 30 | if (match_type == 0) { 31 | // rewrite match type 0 (full key/cert copy) as sha256 32 | br_sha256_context hasher; 33 | br_sha256_init(&hasher); 34 | br_sha256_update(&hasher, cad, cad_len); 35 | br_sha256_out(&hasher, rewritten_cad_buf); 36 | cad = rewritten_cad_buf; 37 | cad_len = 32; 38 | match_type = 1; 39 | } 40 | 41 | const unsigned char *match_sha256, *match_sha512; 42 | if (pinning[1] == 0) { 43 | match_sha256 = cert_sha256; 44 | match_sha512 = cert_sha512; 45 | } else { 46 | match_sha256 = pkey_sha256; 47 | match_sha512 = pkey_sha512; 48 | } 49 | if (match_type==1) { 50 | if (cad_len != 32) continue; 51 | if (!memcmp(cad, match_sha256, 32)) 52 | return i; 53 | } else if (match_type==2) { 54 | if (cad_len != 64) continue; 55 | if (!memcmp(cad, match_sha512, 64)) 56 | return i; 57 | } 58 | } 59 | return -1; 60 | } 61 | -------------------------------------------------------------------------------- /pkey_hash.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static size_t lenlen(size_t k) 4 | { 5 | if (k>=65536) return 4; 6 | if (k>=256) return 3; 7 | if (k>=128) return 2; 8 | return 1; 9 | } 10 | 11 | static size_t derlen(size_t k) 12 | { 13 | return 1+lenlen(k)+k; 14 | } 15 | 16 | static size_t encode(unsigned char *buf, int type, size_t len) 17 | { 18 | buf[0] = type; 19 | if (lenlen(len)==4) { 20 | buf[1] = 0x83; 21 | buf[2] = len>>16; 22 | buf[3] = len>>8; 23 | buf[4] = len; 24 | } else if (lenlen(len)==3) { 25 | buf[1] = 0x82; 26 | buf[2] = len>>8; 27 | buf[3] = len; 28 | } else if (lenlen(len)==2) { 29 | buf[1] = 0x81; 30 | buf[2] = len; 31 | } else { 32 | buf[1] = len; 33 | } 34 | return lenlen(len)+1; 35 | } 36 | 37 | struct hash_ctx { 38 | br_sha256_context sha256; 39 | br_sha512_context sha512; 40 | }; 41 | 42 | static void hash_update(void *ctx, const void *data, size_t len) 43 | { 44 | struct hash_ctx *c = ctx; 45 | br_sha256_update(&c->sha256, data, len); 46 | br_sha512_update(&c->sha512, data, len); 47 | } 48 | 49 | static void hash_rsa(void *ctx, void (*update)(void *, const void *, size_t), unsigned char *n, size_t nlen, unsigned char *e, size_t elen) 50 | { 51 | while (nlen && !*n) nlen--, n++; 52 | while (elen && !*e) elen--, e++; 53 | size_t encoded_elen = derlen(elen+(e[0]>127)); 54 | size_t encoded_nlen = derlen(nlen+(n[0]>127)); 55 | size_t encoded_k_seq_len = derlen(encoded_elen+encoded_nlen); 56 | size_t encoded_k_bs_len = derlen(encoded_k_seq_len+1); 57 | 58 | unsigned char buf[5]; 59 | 60 | // seq 61 | update(ctx, buf, encode(buf, 0x30, 15+encoded_k_bs_len)); 62 | 63 | // seq->objid,null 64 | update(ctx, "\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00", 15); 65 | 66 | // bitstring 67 | update(ctx, buf, encode(buf, 0x03, 1+encoded_k_seq_len)); 68 | update(ctx, "\x00", 1); 69 | 70 | // seq 71 | update(ctx, buf, encode(buf, 0x30, encoded_nlen + encoded_elen)); 72 | 73 | // int 74 | update(ctx, buf, encode(buf, 0x02, nlen+(n[0]>127))); 75 | if (n[0]>127) update(ctx, "\x00", 1); 76 | update(ctx, n, nlen); 77 | 78 | // int 79 | update(ctx, buf, encode(buf, 0x02, elen+(e[0]>127))); 80 | if (e[0]>127) update(ctx, "\x00", 1); 81 | update(ctx, e, elen); 82 | } 83 | 84 | const unsigned char *br_get_curve_OID(int); 85 | 86 | static void hash_ec(void *ctx, void (*update)(void *, const void *, size_t), int curve, unsigned char *q, size_t qlen) 87 | { 88 | size_t encoded_qlen = derlen(qlen+1); 89 | const unsigned char *oid = br_get_curve_OID(curve); 90 | if (!oid) oid = (unsigned char *)""; 91 | size_t encoded_oidlen = derlen(oid[0]); 92 | size_t encoded_seq_len = derlen(9+encoded_oidlen+encoded_qlen); 93 | 94 | unsigned char buf[5]; 95 | 96 | // seq 97 | update(ctx, buf, encode(buf, 0x30, encoded_seq_len)); 98 | 99 | // seq 100 | update(ctx, buf, encode(buf, 0x30, 9+encoded_oidlen)); 101 | 102 | // oid 103 | update(ctx, "\x06\x07\x2a\x86\x48\xce\x3d\x02\x01", 9); 104 | 105 | // oid 106 | update(ctx, "\x06", 1); 107 | update(ctx, oid, oid[0]+1); 108 | 109 | // int 110 | update(ctx, buf, encode(buf, 0x03, qlen+1)); 111 | update(ctx, "", 1); 112 | update(ctx, q, qlen); 113 | } 114 | 115 | void pkey_hash(unsigned char *sha256, unsigned char *sha512, const br_x509_pkey *pkey) 116 | { 117 | struct hash_ctx hc; 118 | br_sha256_init(&hc.sha256); 119 | br_sha512_init(&hc.sha512); 120 | 121 | switch (pkey->key_type) { 122 | case BR_KEYTYPE_RSA: 123 | hash_rsa(&hc, hash_update, pkey->key.rsa.n, pkey->key.rsa.nlen, pkey->key.rsa.e, pkey->key.rsa.elen); 124 | break; 125 | case BR_KEYTYPE_EC: 126 | hash_ec(&hc, hash_update, pkey->key.ec.curve, pkey->key.ec.q, pkey->key.ec.qlen); 127 | break; 128 | } 129 | br_sha256_out(&hc.sha256, sha256); 130 | br_sha512_out(&hc.sha512, sha512); 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mxclient 2 | 3 | mxclient is not a normal MTA. Rather, it's a minimalist client for 4 | sending mail *direct to the recipient's MX*, or mail exchanger, in 5 | contrast to the widespread practice of sending through a "smarthost" 6 | or "outgoing mail server". 7 | 8 | In combination with sufficient cryptographic measures, this ensures 9 | that no one outside the receiving domain's mail system can intercept 10 | or alter the contents of the message, making mxclient suitable for: 11 | 12 | - Private bi-directional communication between individuals (with 13 | personal domains) or organizations that mutually implement this kind 14 | of delivery. 15 | 16 | - Delivery of sensitive data like account access or password reset 17 | tokens without them passing through third party mailer systems. 18 | 19 | - Avoiding dragnet surveillance of outgoing mail in otherwise 20 | conventional mail setups. 21 | 22 | mxclient is not an outgoing mail queue. It delivers mail 23 | synchronously, to a single recipient, reporting success, temporary 24 | failure, or permanent failure via the exit status (using `sysexits.h` 25 | codes). It can be used as the backend for the separate queuing 26 | frontend to yield a full "sendmail" command for use by MUAs or scripts 27 | that expect asynchronous delivery. 28 | 29 | Ability to send mail directly to the recipient's MX depends on having 30 | unblocked outgoing port 25 (many residential and mobile ISPs firewall 31 | it) and on not being on one of several "dialup"/residential IP address 32 | lists that many sites' mail systems use to block likely spammers. To 33 | get around this while still maintaining the security and privacy 34 | properties of interfacing directly with the recipient's MX, future 35 | versions of mxclient will support performing the actual TCP connection 36 | through a (SOCKS5 or `ssh -W` child process) proxy while keeping the 37 | actual TLS endpoint local. 38 | 39 | 40 | ## Project Status 41 | 42 | mxclient is incomplete but under active development. Proxy support is 43 | missing, and DANE modes other than DANE-EE with public key only (vs 44 | full cert) are untested. Otherwise all basic functionality is present. 45 | 46 | 47 | ## Background on SMTP and TLS 48 | 49 | SMTP does not use a separate port/service for TLS-encrypted sessions, 50 | but rather a "STARTTLS" command, advertised in the greeting response, 51 | to upgrade a connection to TLS. Originally this provided only 52 | opportunistic encryption that was easily stripped by MITM devices, and 53 | provided no authentication of the server to the client. Since the CA 54 | infrastructure used on the web does not carry over to SMTP, mail 55 | servers generally used self-signed certificates. 56 | 57 | With DANE and DNSSEC, however, it's possible to have a full chain of 58 | trust for the intended recipient domain. In short, DANE publishes key 59 | or certificate pinnings for a domain in DNS records, and DNSSEC 60 | provides a signature chain proving the authenticity of both the DANE 61 | records and the conventional record types used for mail (MX for the 62 | domain's mail exchangers, and A/AAAA/CNAME records used to find the IP 63 | address of the server to send to). 64 | 65 | mxclient uses the SMTP STARTTLS extension whenever it is advertised by 66 | the server or DANE is in use for the domain, and enforces DANE-EE 67 | unless it can determine non-existence of TLSA (DANE) records for the 68 | recipient domain's MX. It relies on a local DNSSEC-validating 69 | nameserver, ideally on localhost, to obtain this information. 70 | 71 | 72 | ## Building 73 | 74 | The only dependencies for mxclient are 75 | [BearSSL](https://www.bearssl.org/) and a libc with the 76 | `arpa/nameser.h` and `res_query` interfaces. Drop-in replacements for 77 | these can be used on systems that don't have them. 78 | 79 | A `config.mak` file can be created to override default compile/link 80 | flags or install paths. Future versions will ship a `configure` script 81 | that can generate a `config.mak` for you. 82 | 83 | After checking and adusting config as needed, simply run `make`. 84 | mxclient can be installed with `make install`, but installation is not 85 | needed to use it. The program is entirely self-contained and 86 | stand-alone. 87 | 88 | 89 | ## Usage 90 | 91 | Basic usage is: 92 | 93 | mxclient -f you@your.example.com them@their.example.com < message 94 | 95 | where `message` *should* be in standard RFC 822/2822 email message 96 | form, but is not processed locally by mxclient. In particular, a line 97 | containing a lone `.` is not special; input ends only at EOF (like 98 | sendmail with the `-i` option). Either ordinary newlines or CR/LF line 99 | endings (or any mix) are accepted. 100 | 101 | mxclient accepts (and mostly ignores) a few common `sendmail` command 102 | line options, including `-F`, `-i`, and `-o*`. The only option it 103 | actually uses is `-f`, to set the envelope sender (for the `MAIL 104 | FROM:` command). 105 | 106 | Exit code will be 75 for temporary/retryable errors, and another (from 107 | among `sysexits.h` codes) nonzero value for non-retryable errors, or 108 | zero for success. During operation, progress is printed to `stdout`. 109 | -------------------------------------------------------------------------------- /starttls_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void pkey_hash(unsigned char *, unsigned char *, const br_x509_pkey *); 14 | int check_tlsa(const unsigned char *, const unsigned char *, const unsigned char *, const unsigned char *, int, const unsigned char *, size_t); 15 | 16 | struct start_ctx { 17 | int p, s; 18 | const char *hostname; 19 | const unsigned char *tlsa; 20 | size_t tlsa_len; 21 | sem_t sem; 22 | int err; 23 | FILE *errf; 24 | }; 25 | 26 | struct x509_dane_context { 27 | const br_x509_class *vtable; 28 | br_x509_minimal_context minimal; 29 | const unsigned char *tlsa; 30 | size_t tlsa_len; 31 | int trusted; 32 | int chain_idx; 33 | br_x509_decoder_context dec; 34 | br_sha256_context sha256; 35 | br_sha512_context sha512; 36 | const br_x509_pkey *ee_pkey; 37 | FILE *errf; 38 | }; 39 | 40 | static void start_chain(const br_x509_class **ctx, const char *server_name) 41 | { 42 | struct x509_dane_context *c = (void *)ctx; 43 | c->minimal.vtable->start_chain(&c->minimal.vtable, server_name); 44 | } 45 | 46 | static void start_cert(const br_x509_class **ctx, uint32_t length) 47 | { 48 | struct x509_dane_context *c = (void *)ctx; 49 | if (c->trusted) return; 50 | c->minimal.vtable->start_cert(&c->minimal.vtable, length); 51 | br_x509_decoder_init(&c->dec, 0, 0); 52 | br_sha256_init(&c->sha256); 53 | br_sha512_init(&c->sha512); 54 | } 55 | 56 | static void append(const br_x509_class **ctx, const unsigned char *buf, size_t len) 57 | { 58 | struct x509_dane_context *c = (void *)ctx; 59 | if (c->trusted) return; 60 | c->minimal.vtable->append(&c->minimal.vtable, buf, len); 61 | br_x509_decoder_push(&c->dec, buf, len); 62 | br_sha256_update(&c->sha256, buf, len); 63 | br_sha512_update(&c->sha512, buf, len); 64 | } 65 | 66 | static void print_tlsa(FILE *f, const unsigned char *tlsa, size_t tlsa_len, int idx) 67 | { 68 | ns_msg msg; 69 | if (ns_initparse(tlsa, tlsa_len, &msg) < 0) return; 70 | ns_rr rr; 71 | if (!ns_parserr(&msg, ns_s_an, idx, &rr)) { 72 | const unsigned char *data = ns_rr_rdata(rr); 73 | size_t len = ns_rr_rdlen(rr); 74 | fprintf(f, "%d %d %d ", data[0], data[1], data[2]); 75 | for (int i=3; itrusted) return; 85 | c->minimal.vtable->end_cert(&c->minimal.vtable); 86 | 87 | const br_x509_pkey *pkey = br_x509_decoder_get_pkey(&c->dec); 88 | unsigned char pkey_sha256[32], pkey_sha512[64]; 89 | pkey_hash(pkey_sha256, pkey_sha512, pkey); 90 | 91 | unsigned char cert_sha256[32], cert_sha512[64]; 92 | br_sha256_out(&c->sha256, cert_sha256); 93 | br_sha512_out(&c->sha512, cert_sha512); 94 | 95 | int r = check_tlsa(pkey_sha256, pkey_sha512, cert_sha256, cert_sha512, !c->chain_idx, c->tlsa, c->tlsa_len); 96 | if (r>=0) { 97 | c->trusted = 1; 98 | if (!c->chain_idx) c->ee_pkey = pkey; 99 | if (c->errf) { 100 | if (!c->tlsa_len) { 101 | fprintf(c->errf, "No trust anchor; accepted key "); 102 | for (int i=0; i<32; i++) fprintf(c->errf, "%.2X", pkey_sha256[i]); 103 | fprintf(c->errf, "\n"); 104 | } else { 105 | if (c->chain_idx) 106 | fprintf(c->errf, "Accepted trust anchor certificate at position %d matching DANE record:\n", c->chain_idx); 107 | else 108 | fprintf(c->errf, "Accepted end entity certificate matching DANE record:\n"); 109 | print_tlsa(c->errf, c->tlsa, c->tlsa_len, r); 110 | } 111 | } 112 | } 113 | c->chain_idx++; 114 | } 115 | 116 | static unsigned end_chain(const br_x509_class **ctx) 117 | { 118 | struct x509_dane_context *c = (void *)ctx; 119 | if (c->ee_pkey) return 0; 120 | unsigned r = c->minimal.vtable->end_chain(&c->minimal.vtable); 121 | if (r && r != BR_ERR_X509_NOT_TRUSTED) return r; 122 | return c->trusted ? 0 : BR_ERR_X509_NOT_TRUSTED; 123 | } 124 | 125 | static const br_x509_pkey *get_pkey(const br_x509_class *const *ctx, unsigned *usages) 126 | { 127 | struct x509_dane_context *c = (void *)ctx; 128 | if (c->ee_pkey) { 129 | if (usages) *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; // ?? 130 | return c->ee_pkey; 131 | } 132 | return c->minimal.vtable->get_pkey(&c->minimal.vtable, usages); 133 | } 134 | 135 | static const br_x509_class x509_dane_vtable = { 136 | .context_size = sizeof(struct x509_dane_context), 137 | .start_chain = start_chain, 138 | .start_cert = start_cert, 139 | .append = append, 140 | .end_cert = end_cert, 141 | .end_chain = end_chain, 142 | .get_pkey = get_pkey, 143 | }; 144 | 145 | struct vt_wrap { 146 | br_x509_class vt; 147 | unsigned (*old_end_chain)(const br_x509_class **ctx); 148 | const unsigned char *tlsa; 149 | size_t tlsa_len; 150 | }; 151 | 152 | static void *tlsthread(void *vc) 153 | { 154 | struct start_ctx *ctx = vc; 155 | int s = ctx->s, p = ctx->p; 156 | 157 | br_ssl_client_context sc; 158 | struct x509_dane_context xc = { 159 | .vtable = &x509_dane_vtable, 160 | .tlsa = ctx->tlsa, 161 | .tlsa_len = ctx->tlsa_len, 162 | .errf = ctx->errf, 163 | }; 164 | br_ssl_client_init_full(&sc, &xc.minimal, 0, 0); 165 | br_ssl_engine_set_x509(&sc.eng, &xc.vtable); 166 | 167 | unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; 168 | br_ssl_engine_set_buffer(&sc.eng, iobuf, sizeof iobuf, 1); 169 | 170 | br_ssl_client_reset(&sc, ctx->hostname, 0); 171 | 172 | struct timeval no_to = { 0 }; 173 | setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &no_to, sizeof no_to); 174 | setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &no_to, sizeof no_to); 175 | fcntl(p, F_SETFL, fcntl(p, F_GETFL) | O_NONBLOCK); 176 | 177 | int started = 0; 178 | 179 | for (;;) { 180 | unsigned st = br_ssl_engine_current_state(&sc.eng); 181 | struct pollfd pfd[2] = { { .fd = p }, { .fd = s } }; 182 | if (!started) { 183 | if (st == BR_SSL_CLOSED) { 184 | if (ctx->errf) 185 | fprintf(ctx->errf, "BearSSL error %d\n", 186 | br_ssl_engine_last_error(&sc.eng)); 187 | ctx->err = 1; 188 | sem_post(&ctx->sem); 189 | return 0; 190 | } 191 | if (st & BR_SSL_SENDAPP) { 192 | ctx->err = 0; 193 | sem_post(&ctx->sem); 194 | started = 1; 195 | } 196 | } 197 | if (st == BR_SSL_CLOSED) { 198 | //int err = br_ssl_engine_last_error(&sc.eng); 199 | break; 200 | } 201 | if (st & BR_SSL_SENDREC) 202 | pfd[1].events |= POLLOUT; 203 | if (st & BR_SSL_RECVREC) 204 | pfd[1].events |= POLLIN; 205 | if (st & BR_SSL_SENDAPP) 206 | pfd[0].events |= POLLIN; 207 | if (st & BR_SSL_RECVAPP) 208 | pfd[0].events |= POLLOUT; 209 | if (poll(pfd, 2, -1) < 1) continue; 210 | if (pfd[0].revents & POLLIN) { 211 | size_t len; 212 | unsigned char *buf = br_ssl_engine_sendapp_buf(&sc.eng, &len); 213 | len = read(p, buf, len); 214 | if (!len || len==-1) break; 215 | br_ssl_engine_sendapp_ack(&sc.eng, len); 216 | br_ssl_engine_flush(&sc.eng, 0); 217 | continue; 218 | } 219 | if (pfd[0].revents & POLLOUT) { 220 | size_t len; 221 | unsigned char *buf = br_ssl_engine_recvapp_buf(&sc.eng, &len); 222 | len = write(p, buf, len); 223 | if (!len || len==-1) break; 224 | br_ssl_engine_recvapp_ack(&sc.eng, len); 225 | continue; 226 | } 227 | if (pfd[1].revents & POLLOUT) { 228 | size_t len; 229 | unsigned char *buf = br_ssl_engine_sendrec_buf(&sc.eng, &len); 230 | len = write(s, buf, len); 231 | if (!len || len==-1) break; 232 | br_ssl_engine_sendrec_ack(&sc.eng, len); 233 | continue; 234 | } 235 | if (pfd[1].revents & POLLIN) { 236 | size_t len; 237 | unsigned char *buf = br_ssl_engine_recvrec_buf(&sc.eng, &len); 238 | len = read(s, buf, len); 239 | if (!len || len==-1) break; 240 | br_ssl_engine_recvrec_ack(&sc.eng, len); 241 | continue; 242 | } 243 | } 244 | if (!started) { 245 | ctx->err = 1; 246 | sem_post(&ctx->sem); 247 | } 248 | close(s); 249 | close(p); 250 | return 0; 251 | } 252 | 253 | int starttls_client(int s, const char *hostname, const unsigned char *tlsa, size_t tlsa_len, FILE *errf) 254 | { 255 | s = fcntl(s, F_DUPFD_CLOEXEC, 0); 256 | if (s < 0) return -1; 257 | 258 | struct timeval sto = { 0 }, rto = { 0 }; 259 | getsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &rto, &(socklen_t){ sizeof rto }); 260 | getsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &sto, &(socklen_t){ sizeof sto }); 261 | 262 | int sp[2]; 263 | if (!socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, sp)) { 264 | setsockopt(sp[0], SOL_SOCKET, SO_RCVTIMEO, &rto, sizeof rto); 265 | setsockopt(sp[0], SOL_SOCKET, SO_SNDTIMEO, &sto, sizeof sto); 266 | 267 | struct start_ctx ctx; 268 | sem_init(&ctx.sem, 0, 0); 269 | ctx.s = s; 270 | ctx.p = sp[1]; 271 | ctx.hostname = hostname; 272 | ctx.tlsa = tlsa; 273 | ctx.tlsa_len = tlsa_len; 274 | ctx.errf = errf; 275 | pthread_t td; 276 | if (!pthread_create(&td, 0, tlsthread, &ctx)) { 277 | struct timespec ts; 278 | clock_gettime(CLOCK_REALTIME, &ts); 279 | ts.tv_sec += sto.tv_sec; 280 | if ((ts.tv_nsec += sto.tv_usec * 1000) > 1000000000) { 281 | ts.tv_sec++; 282 | ts.tv_nsec -= 1000000000; 283 | } 284 | if (sem_timedwait(&ctx.sem, &ts)) { 285 | shutdown(s, SHUT_RDWR); 286 | pthread_join(td, 0); 287 | return -1; 288 | } 289 | if (!ctx.err) { 290 | pthread_detach(td); 291 | return sp[0]; 292 | } 293 | pthread_join(td, 0); 294 | } 295 | close(sp[0]); 296 | close(sp[1]); 297 | } 298 | close(s); 299 | return -1; 300 | 301 | } 302 | -------------------------------------------------------------------------------- /mxclient.c: -------------------------------------------------------------------------------- 1 | /* 2 | * mxclient - a minimalist, direct-to-recipient-mx smtp client 3 | * 4 | * Copyright © 2020 Rich Felker 5 | * 6 | * SPDX-License-Identifier: MIT 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | static int open_smtp_socket(const char *hostname) 25 | { 26 | struct addrinfo *ai, *ai0; 27 | int r = getaddrinfo(hostname, "25", &(struct addrinfo){.ai_socktype = SOCK_STREAM}, &ai); 28 | if (r == EAI_NONAME) return -EX_NOHOST; 29 | if (r) return -EX_TEMPFAIL; 30 | for (ai0=ai; ai; ai=ai->ai_next) { 31 | int s = socket(ai->ai_family, ai->ai_socktype|SOCK_CLOEXEC, ai->ai_protocol); 32 | if (!connect(s, ai->ai_addr, ai->ai_addrlen)) 33 | return s; 34 | close(s); 35 | } 36 | freeaddrinfo(ai0); 37 | return -EX_TEMPFAIL; 38 | } 39 | 40 | int intcmp(const void *pa, const void *pb) 41 | { 42 | int a = *(const int *)pa; 43 | int b = *(const int *)pb; 44 | if (ab) return 1; 46 | return 0; 47 | } 48 | 49 | static int open_mx_socket(const char *domain, char *hostname) 50 | { 51 | if (strlen(domain) >= HOST_NAME_MAX) return -1; 52 | 53 | unsigned char qbuf[HOST_NAME_MAX+50]; 54 | unsigned char abuf[1024]; 55 | int qlen = res_mkquery(0, domain, 1, T_MX, 0, 0, 0, qbuf, sizeof qbuf); 56 | if (qlen < 0) return -EX_TEMPFAIL; 57 | int alen = res_send(qbuf, qlen, abuf, sizeof abuf); 58 | if (alen < 0) return -EX_TEMPFAIL; 59 | if (alen > sizeof abuf) alen = sizeof abuf; 60 | 61 | ns_msg msg; 62 | int r = ns_initparse(abuf, alen, &msg); 63 | if (r<0) return -EX_TEMPFAIL; 64 | ns_rr rr; 65 | if (ns_msg_getflag(msg, ns_f_rcode) == ns_r_nxdomain) 66 | return -EX_NOHOST; 67 | if (ns_msg_getflag(msg, ns_f_rcode)) 68 | return -EX_TEMPFAIL; 69 | int mxsort[sizeof abuf / 12][2], cnt=0; 70 | for (int i=0; !ns_parserr(&msg, ns_s_an, i, &rr); i++) { 71 | if (ns_rr_type(rr) != T_MX) continue; 72 | mxsort[cnt][0] = ns_rr_rdata(rr)[0]*256 + ns_rr_rdata(rr)[1]; 73 | mxsort[cnt++][1] = i; 74 | } 75 | if (!cnt) { 76 | strcpy(hostname, domain); 77 | int s = open_smtp_socket(hostname); 78 | return s; 79 | } 80 | qsort(mxsort, cnt, sizeof *mxsort, intcmp); 81 | for (int i=0; i=0) return s; 87 | } 88 | return -EX_TEMPFAIL; 89 | } 90 | 91 | static int is_insecure(const char *hostname) 92 | { 93 | unsigned char query[HOST_NAME_MAX+50]; 94 | unsigned char answer[1024]; 95 | int qlen, alen, r; 96 | ns_msg msg; 97 | ns_rr rr; 98 | int rrtype[2] = { 1 /* A */, 5 /* CNAME */ }; 99 | 100 | for (int i=0; i<2; i++) { 101 | qlen = res_mkquery(0, hostname, 1, rrtype[i], 102 | 0, 0, 0, query, sizeof query); 103 | if (qlen < 0) return 0; 104 | query[3] |= 32; /* AD flag */ 105 | 106 | alen = res_send(query, qlen, answer, sizeof answer); 107 | if (alen < 0) return 0; 108 | if (alen > sizeof answer) alen = sizeof answer; 109 | 110 | r = ns_initparse(answer, alen, &msg); 111 | if (r < 0) return 0; 112 | 113 | r = ns_msg_getflag(msg, ns_f_rcode); 114 | if (r != ns_r_nxdomain && r != ns_r_noerror) return 0; 115 | 116 | if (ns_msg_getflag(msg, ns_f_ad)) return 0; 117 | 118 | if (rrtype[i] == 5) break; 119 | 120 | int is_cname = 0; 121 | for (int j=0; !ns_parserr(&msg, ns_s_an, j, &rr); j++) 122 | if (ns_rr_type(rr) == 5) is_cname = 1; 123 | if (!is_cname) break; 124 | } 125 | return 1; 126 | } 127 | 128 | static int get_tlsa(unsigned char *tlsa, size_t maxsize, const char *hostname, FILE *f) 129 | { 130 | char buf[HOST_NAME_MAX+20]; 131 | snprintf(buf, sizeof buf, "_25._tcp.%s", hostname); 132 | unsigned char query[HOST_NAME_MAX+50]; 133 | int qlen = res_mkquery(0, buf, 1, 52 /* TLSA */, 0, 0, 0, query, sizeof query); 134 | if (qlen < 0) return -EX_DATAERR; 135 | query[3] |= 32; /* AD flag */ 136 | int alen = res_send(query, qlen, tlsa, maxsize); 137 | if (alen < 0) goto tempfail; 138 | if (alen > maxsize) alen = maxsize; 139 | 140 | ns_msg msg; 141 | int r = ns_initparse(tlsa, alen, &msg); 142 | if (r<0) goto tempfail; 143 | ns_rr rr; 144 | if (ns_msg_getflag(msg, ns_f_rcode) == ns_r_nxdomain) 145 | return 0; 146 | if (ns_msg_getflag(msg, ns_f_rcode) != ns_r_noerror) { 147 | /* in case error is caused by broken auth ns for the domain 148 | * failing to understand TLSA query, check to determine 149 | * if zone is insecure (unsigned) and conclude no valid 150 | * TLSA records */ 151 | tempfail: 152 | if (f) fprintf(f, "%s TLSA lookup failed, checking DNSSEC status\n", hostname); 153 | if (is_insecure(hostname)) return 0; 154 | if (f) fprintf(f, "%s cannot be determined insecure; delivery not possible\n", hostname); 155 | return -EX_TEMPFAIL; 156 | } 157 | if (!ns_msg_getflag(msg, ns_f_ad)) 158 | return 0; 159 | for (int i=0; !ns_parserr(&msg, ns_s_an, i, &rr); i++) { 160 | if (ns_rr_type(rr) != 52) continue; 161 | return alen; 162 | } 163 | return 0; 164 | } 165 | 166 | int starttls_client(int, const char *, const unsigned char *, size_t, FILE *); 167 | 168 | #define d2printf(fd, ...) (printf(">>> " __VA_ARGS__), dprintf(fd, __VA_ARGS__)) 169 | #define fgets_echo(buf, size, f) (fgets(buf, size, f) ? printf("<<< %s", buf), buf : 0) 170 | 171 | int getresponse(char *buf, int size, FILE *f) 172 | { 173 | do { 174 | if (!fgets(buf, size, f)) return -1; 175 | printf("<<< %s", buf); 176 | } while (!buf[0] || !buf[1] || !buf[2] || buf[3]=='-'); 177 | return 0; 178 | } 179 | 180 | int main(int argc, char **argv) 181 | { 182 | const char *from_addr = ""; 183 | int c; 184 | while ((c=getopt(argc, argv, "o:F:f:i")) > 0) switch (c) { 185 | case 'F': 186 | break; 187 | case 'f': 188 | from_addr = optarg; 189 | break; 190 | case 'o': 191 | if (optarg[0] != 'i' || optarg[1]) break; 192 | case 'i': 193 | break; 194 | } 195 | 196 | const char *to = argv[optind]; 197 | if (!to) { 198 | fprintf(stderr, "%s: missing recipient\n", argv[0]); 199 | return EX_USAGE; 200 | } 201 | 202 | signal(SIGPIPE, SIG_IGN); 203 | 204 | int tls = 0, tls_done = 0; 205 | char mx_hostname[HOST_NAME_MAX+1]; 206 | char helo_host[HOST_NAME_MAX+1]; 207 | unsigned char tlsa[65536]; 208 | 209 | gethostname(helo_host, sizeof helo_host); 210 | const char *domain = strchr(to, '@'); 211 | char buf[1024]; 212 | if (!domain) return EX_USAGE; 213 | domain++; 214 | 215 | int s = open_mx_socket(domain, mx_hostname); 216 | if (s < 0) return -s; 217 | 218 | int tlsa_len = get_tlsa(tlsa, sizeof tlsa, mx_hostname, stdout); 219 | 220 | /* failure to obtain DANE records or negative result must be fatal */ 221 | if (tlsa_len < 0) return -tlsa_len; 222 | 223 | /* force tls if there is a tlsa record */ 224 | if (tlsa_len) { 225 | printf("%s has DANE records, forcing STARTTLS\n", mx_hostname); 226 | tls = 1; 227 | } else { 228 | printf("%s has no DANE records, STARTTLS opportunistic\n", mx_hostname); 229 | } 230 | 231 | struct timeval timeout = { .tv_sec = 60 }; 232 | setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout); 233 | setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout); 234 | 235 | FILE *f = fdopen(dup(s), "rb"); 236 | if (getresponse(buf, sizeof buf, f)) goto rderr; 237 | if (buf[0]!='2') goto fail; 238 | 239 | restart: 240 | if (d2printf(s, "EHLO %s\r\n", helo_host) < 0) goto wrerr; 241 | for (;;) { 242 | if (!fgets_echo(buf, sizeof buf, f)) goto rderr; 243 | if (buf[0]!='2' || !buf[1] || !buf[2] || !buf[3]) goto fail; 244 | if (!strncmp(buf+4, "STARTTLS", 8)) tls = 1; 245 | if (buf[3]==' ') break; 246 | } 247 | 248 | if (tls && !tls_done) { 249 | if (d2printf(s, "STARTTLS\r\n") < 0) goto wrerr; 250 | if (getresponse(buf, sizeof buf, f)) goto rderr; 251 | if (buf[0]!='2') goto fail; 252 | 253 | int tls_s = starttls_client(s, mx_hostname, tlsa, tlsa_len, stdout); 254 | if (tls_s < 0) { 255 | printf("STARTTLS failed\n"); 256 | return EX_TEMPFAIL; 257 | } 258 | s = tls_s; 259 | dup2(s, fileno(f)); 260 | tls_done = 1; 261 | goto restart; 262 | } 263 | 264 | if (d2printf(s, "MAIL FROM:<%s>\r\n", from_addr) < 0) goto wrerr; 265 | if (getresponse(buf, sizeof buf, f)) goto rderr; 266 | if (buf[0]!='2') goto fail; 267 | 268 | if (d2printf(s, "RCPT TO:<%s>\r\n", to) < 0) goto wrerr; 269 | if (getresponse(buf, sizeof buf, f)) goto rderr; 270 | if (buf[0]!='2') goto fail; 271 | 272 | if (d2printf(s, "DATA\r\n") < 0) goto wrerr; 273 | if (getresponse(buf, sizeof buf, f)) goto rderr; 274 | if (buf[0]!='3') goto fail; 275 | 276 | FILE *f2 = fdopen(dup(s), "wb"); 277 | while (fgets(buf, sizeof buf, stdin)) { 278 | size_t l = strlen(buf); 279 | if (l && buf[l-1]=='\n') l--; 280 | if (l && buf[l-1]=='\r') l--; 281 | if (buf[0]=='.') putc('.', f2); 282 | if (fprintf(f2, "%.*s\r\n", (int)l, buf)<0) goto wrerr; 283 | } 284 | if (ferror(stdin)) { 285 | fprintf(stderr, "%s: error reading input: %s\n", 286 | argv[0], strerror(errno)); 287 | } 288 | fprintf(f2, ".\r\n"); 289 | if (fclose(f2) < 0) goto wrerr; 290 | 291 | if (getresponse(buf, sizeof buf, f)) goto rderr; 292 | if (buf[0]!='2') goto fail; 293 | 294 | return 0; 295 | 296 | fail: 297 | if (buf[0]=='4') return EX_TEMPFAIL; 298 | return EX_PROTOCOL; 299 | 300 | wrerr: 301 | fprintf(stderr, "%s: error writing to socket: %s\n", 302 | argv[0], strerror(errno)); 303 | return EX_TEMPFAIL; 304 | 305 | rderr: 306 | fprintf(stderr, "%s: error reading from socket: %s\n", 307 | argv[0], strerror(errno)); 308 | return EX_TEMPFAIL; 309 | } 310 | --------------------------------------------------------------------------------