Blob


1 /*
2 * Copyright (c) 2019 Martijn van Duren <martijn@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16 #include <sys/tree.h>
18 #include <openssl/evp.h>
19 #include <openssl/pem.h>
20 #include <openssl/sha.h>
22 #include <ctype.h>
23 #include <err.h>
24 #include <fcntl.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <syslog.h>
29 #include <time.h>
30 #include <unistd.h>
32 #include "log.h"
33 #include "smtp_proc.h"
35 struct dkim_signature {
36 char *signature;
37 size_t size;
38 size_t len;
39 };
41 struct dkim_session {
42 uint64_t reqid;
43 uint64_t token;
44 FILE *origf;
45 int parsing_headers;
46 char **headers;
47 int lastheader;
48 size_t body_whitelines;
49 int has_body;
50 struct dkim_signature signature;
51 EVP_MD_CTX *b;
52 EVP_MD_CTX *bh;
53 RB_ENTRY(dkim_session) entry;
54 };
56 RB_HEAD(dkim_sessions, dkim_session) dkim_sessions = RB_INITIALIZER(NULL);
57 RB_PROTOTYPE(dkim_sessions, dkim_session, entry, dkim_session_cmp);
59 /* RFC 6376 section 5.4.1 */
60 static char *dsign_headers[] = {
61 "from",
62 "reply-to",
63 "subject",
64 "date",
65 "to",
66 "cc",
67 "resent-date",
68 "resent-from",
69 "resent-to",
70 "resent-cc",
71 "in-reply-to",
72 "references",
73 "list-id",
74 "list-help",
75 "list-unsubscribe",
76 "list-subscribe",
77 "list-post",
78 "list-owner",
79 "list-archive"
80 };
81 static char **sign_headers = dsign_headers;
82 static size_t nsign_headers = sizeof(dsign_headers) / sizeof(*dsign_headers);
84 static char *hashalg = "sha256";
85 static char *cryptalg = "rsa";
87 #define CANON_SIMPLE 0
88 #define CANON_RELAXED 1
89 static int canonheader = CANON_SIMPLE;
90 static int canonbody = CANON_SIMPLE;
92 static char *domain = NULL;
93 static char *selector = NULL;
95 static EVP_PKEY *pkey;
96 static const EVP_MD *hash_md;
98 #define DKIM_SIGNATURE_LINELEN 78
100 void usage(void);
101 void dkim_err(struct dkim_session *, char *);
102 void dkim_errx(struct dkim_session *, char *);
103 void dkim_headers_set(char *);
104 void dkim_dataline(char *, int, struct timespec *, char *, char *, uint64_t,
105 uint64_t, char *);
106 void dkim_disconnect(char *, int, struct timespec *, char *, char *, uint64_t);
107 struct dkim_session *dkim_session_new(uint64_t);
108 void dkim_session_free(struct dkim_session *);
109 int dkim_session_cmp(struct dkim_session *, struct dkim_session *);
110 void dkim_parse_header(struct dkim_session *, char *, int);
111 void dkim_parse_body(struct dkim_session *, char *);
112 int dkim_signature_printf(struct dkim_session *, char *, ...)
113 __attribute__((__format__ (printf, 2, 3)));
114 int dkim_signature_normalize(struct dkim_session *);
115 int dkim_signature_need(struct dkim_session *, size_t);
116 int dkim_sign_init(struct dkim_session *);
118 int
119 main(int argc, char *argv[])
121 int ch;
122 int i;
123 int debug = 0;
124 FILE *keyfile;
126 while ((ch = getopt(argc, argv, "a:c:Dd:h:k:s:")) != -1) {
127 switch (ch) {
128 case 'a':
129 if (strncmp(optarg, "rsa-", 4) != 0)
130 err(1, "invalid algorithm");
131 hashalg = optarg + 4;
132 break;
133 case 'c':
134 if (strncmp(optarg, "simple", 6) == 0) {
135 canonheader = CANON_SIMPLE;
136 optarg += 6;
137 } else if (strncmp(optarg, "relaxed", 7) == 0) {
138 canonheader = CANON_RELAXED;
139 optarg += 7;
140 } else
141 err(1, "Invalid canonicalization");
142 if (optarg[0] == '/') {
143 if (strcmp(optarg + 1, "simple") == 0)
144 canonbody = CANON_SIMPLE;
145 else if (strcmp(optarg + 1, "relaxed") == 0)
146 canonbody = CANON_RELAXED;
147 else
148 err(1, "Invalid canonicalization");
149 } else if (optarg[0] == '\0')
150 canonbody = CANON_SIMPLE;
151 else
152 err(1, "Invalid canonicalization");
153 break;
154 case 'd':
155 domain = optarg;
156 break;
157 case 'h':
158 dkim_headers_set(optarg);
159 break;
160 case 'k':
161 if ((keyfile = fopen(optarg, "r")) == NULL)
162 err(1, "Can't open key file");
163 pkey = PEM_read_PrivateKey(keyfile, NULL, NULL, NULL);
164 if (pkey == NULL)
165 errx(1, "Can't read key file");
166 if (EVP_PKEY_get0_RSA(pkey) == NULL)
167 err(1, "Key is not of type rsa");
168 fclose(keyfile);
169 break;
170 case 's':
171 selector = optarg;
172 break;
173 case 'D':
174 debug = 1;
175 break;
176 default:
177 usage();
180 log_init(debug, LOG_MAIL);
182 OpenSSL_add_all_digests();
183 if ((hash_md = EVP_get_digestbyname(hashalg)) == NULL)
184 fatalx("Can't find hash: %s", hashalg);
186 if (pledge("tmppath stdio", NULL) == -1)
187 fatal("pledge");
189 if (domain == NULL || selector == NULL || pkey == NULL)
190 usage();
192 smtp_register_filter_dataline(dkim_dataline);
193 smtp_in_register_report_disconnect(dkim_disconnect);
194 smtp_run(debug);
196 return 0;
199 void
200 dkim_disconnect(char *type, int version, struct timespec *tm, char *direction,
201 char *phase, uint64_t reqid)
203 struct dkim_session *session, search;
205 search.reqid = reqid;
206 if ((session = RB_FIND(dkim_sessions, &dkim_sessions, &search)) != NULL)
207 dkim_session_free(session);
210 void
211 dkim_dataline(char *type, int version, struct timespec *tm, char *direction,
212 char *phase, uint64_t reqid, uint64_t token, char *line)
214 struct dkim_session *session, search;
215 struct dkim_signature sig;
216 /* Use largest hash size her */
217 char bbh[EVP_MAX_MD_SIZE];
218 char bh[(((sizeof(bbh) + 2) / 3) * 4) + 1];
219 char *b;
220 ssize_t i, j;
221 size_t linelen;
222 char *tmp, *tmp2;
223 char tmpchar;
225 search.reqid = reqid;
226 session = RB_FIND(dkim_sessions, &dkim_sessions, &search);
227 if (session == NULL) {
228 if ((session = dkim_session_new(reqid)) == NULL)
229 return;
230 session->token = token;
231 } else if (session->token != token)
232 fatalx("Token incorrect");
234 linelen = strlen(line);
235 if (fprintf(session->origf, "%s\n", line) < linelen)
236 dkim_err(session, "Couldn't write to tempfile");
238 if (line[0] == '.' && line[1] =='\0') {
239 /* This entire section needs an error handling revamp */
240 if (canonbody == CANON_SIMPLE && !session->has_body) {
241 if (EVP_DigestUpdate(session->bh, "\r\n", 2) <= 0) {
242 dkim_err(session, "Can't update hash context");
243 return;
246 if (EVP_DigestFinal_ex(session->bh, bbh, NULL) == 0) {
247 dkim_err(session, "Can't finalize hash context");
248 return;
250 EVP_EncodeBlock(bh, bbh, EVP_MD_CTX_size(session->bh));
251 if (!dkim_signature_printf(session, "bh=%s; h=", bh))
252 return;
253 /* Reverse order for ease of use of RFC6367 section 5.4.2 */
254 for (i = 0; session->headers[i] != NULL; i++)
255 continue;
256 for (i--; i >= 0; i--) {
257 if (EVP_DigestSignUpdate(session->b,
258 session->headers[i],
259 strlen(session->headers[i])) <= 0 ||
260 EVP_DigestSignUpdate(session->b,
261 "\r\n", 2) <= 0) {
262 dkim_errx(session,
263 "Failed to update digest context");
264 return;
266 /* We're done with the cashed header after hashing */
267 for (tmp = session->headers[i]; tmp[0] != ':'; tmp++) {
268 if (tmp[0] == ' ' || tmp[0] == '\t')
269 break;
270 tmp[0] = tolower(tmp[0]);
272 tmp[0] = '\0';
273 if (!dkim_signature_printf(session, "%s%s",
274 session->headers[i + 1] == NULL ? "" : ":",
275 session->headers[i]))
276 return;
277 tmp[0] = tmpchar;
279 dkim_signature_printf(session, "; b=");
280 if (!dkim_signature_normalize(session))
281 return;
282 if ((tmp = strdup(session->signature.signature)) == NULL) {
283 dkim_err(session, "Can't create DKIM signature");
284 return;
286 dkim_parse_header(session, tmp, 1);
287 if (EVP_DigestSignUpdate(session->b, tmp,
288 strlen(tmp)) <= 0) {
289 dkim_err(session, "Failed to update digest context");
290 return;
292 free(tmp);
293 if (EVP_DigestSignFinal(session->b, NULL, &linelen) <= 0) {
294 dkim_err(session, "Failed to finalize digest");
295 return;
297 if ((tmp = malloc(linelen)) == NULL) {
298 dkim_err(session, "Can't allocate space for digest");
299 return;
301 if (EVP_DigestSignFinal(session->b, tmp, &linelen) <= 0) {
302 dkim_err(session, "Failed to finalize digest");
303 return;
305 /* Lines are unlikely to overflow */
306 b = malloc((((linelen + 2) / 3) * 4) + 1);
307 EVP_EncodeBlock(b, tmp, linelen);
308 free(tmp);
309 dkim_signature_printf(session, "%s\r\n", b);
310 free(b);
311 dkim_signature_normalize(session);
312 tmp = session->signature.signature;
313 while ((tmp2 = strchr(tmp, '\r')) != NULL) {
314 tmp2[0] = '\0';
315 smtp_filter_dataline(session->reqid, session->token,
316 "%s", tmp);
317 tmp = tmp2 + 2;
319 tmp = NULL;
320 linelen = 0;
321 rewind(session->origf);
322 while ((i = getline(&tmp, &linelen, session->origf)) != -1) {
323 tmp[i - 1] = '\0';
324 smtp_filter_dataline(session->reqid, session->token,
325 "%s", tmp);
327 dkim_session_free(session);
328 } else if (linelen != 0 && session->parsing_headers) {
329 if (line[0] == '.')
330 line++;
331 dkim_parse_header(session, line, 0);
332 } else if (linelen == 0 && session->parsing_headers) {
333 session->parsing_headers = 0;
334 } else {
335 if (line[0] == '.')
336 line++;
337 dkim_parse_body(session, line);
341 struct dkim_session *
342 dkim_session_new(uint64_t reqid)
344 struct dkim_session *session;
345 struct dkim_signature *signature;
346 char origfile[] = "/tmp/filter-dkimXXXXXX";
347 int fd;
349 if ((session = calloc(1, sizeof(*session))) == NULL)
350 fatal(NULL);
352 session->reqid = reqid;
353 if ((fd = mkstemp(origfile)) == -1) {
354 dkim_err(session, "Can't open tempfile");
355 return NULL;
357 if (unlink(origfile) == -1)
358 log_warn("Failed to unlink tempfile %s", origfile);
359 if ((session->origf = fdopen(fd, "r+")) == NULL) {
360 dkim_err(session, "Can't open tempfile");
361 return NULL;
363 session->parsing_headers = 1;
365 session->body_whitelines = 0;
366 session->headers = calloc(1, sizeof(*(session->headers)));
367 if (session->headers == NULL) {
368 dkim_err(session, "Can't save headers");
369 return NULL;
371 session->lastheader = 0;
372 session->signature.signature = NULL;
373 session->signature.size = 0;
374 session->signature.len = 0;
376 if (!dkim_signature_printf(session,
377 "DKIM-signature: v=%s; a=%s-%s; c=%s/%s; d=%s; s=%s; ", "1",
378 cryptalg, hashalg,
379 canonheader == CANON_SIMPLE ? "simple" : "relaxed",
380 canonbody == CANON_SIMPLE ? "simple" : "relaxed",
381 domain, selector))
382 return NULL;
384 if ((session->b = EVP_MD_CTX_new()) == NULL ||
385 (session->bh = EVP_MD_CTX_new()) == NULL) {
386 dkim_errx(session, "Can't create hash context");
387 return NULL;
389 if (EVP_DigestSignInit(session->b, NULL, hash_md, NULL, pkey) <= 0 ||
390 EVP_DigestInit_ex(session->bh, hash_md, NULL) == 0) {
391 dkim_errx(session, "Failed to initialize hash context");
392 return NULL;
394 if (RB_INSERT(dkim_sessions, &dkim_sessions, session) != NULL)
395 fatalx("session already registered");
396 return session;
399 void
400 dkim_session_free(struct dkim_session *session)
402 size_t i;
404 RB_REMOVE(dkim_sessions, &dkim_sessions, session);
405 fclose(session->origf);
406 EVP_MD_CTX_free(session->b);
407 EVP_MD_CTX_free(session->bh);
408 free(session->signature.signature);
409 for (i = 0; session->headers[i] != NULL; i++)
410 free(session->headers[i]);
411 free(session->headers);
412 free(session);
415 int
416 dkim_session_cmp(struct dkim_session *s1, struct dkim_session *s2)
418 return (s1->reqid < s2->reqid ? -1 : s1->reqid > s2->reqid);
421 void
422 dkim_headers_set(char *headers)
424 size_t i;
425 int has_from = 0;
427 nsign_headers = 1;
429 for (i = 0; headers[i] != '\0'; i++) {
430 /* RFC 5322 field-name */
431 if (!(headers[i] >= 33 && headers[i] <= 126))
432 errx(1, "-h: invalid character");
433 if (headers[i] == ':') {
434 /* Test for empty headers */
435 if (i == 0 || headers[i - 1] == ':')
436 errx(1, "-h: header can't be empty");
437 nsign_headers++;
439 headers[i] = tolower(headers[i]);
441 if (headers[i - 1] == ':')
442 errx(1, "-h: header can't be empty");
444 sign_headers = reallocarray(NULL, nsign_headers + 1, sizeof(*sign_headers));
445 if (sign_headers == NULL)
446 errx(1, NULL);
448 for (i = 0; i < nsign_headers; i++) {
449 sign_headers[i] = headers;
450 if (i != nsign_headers - 1) {
451 headers = strchr(headers, ':');
452 headers++[0] = '\0';
454 if (strcasecmp(sign_headers[i], "from") == 0)
455 has_from = 1;
457 if (!has_from)
458 errx(1, "From header must be included");
461 void
462 dkim_err(struct dkim_session *session, char *msg)
464 smtp_filter_disconnect(session->reqid, session->token,
465 "Internal server error");
466 log_warn("%s", msg);
467 dkim_session_free(session);
470 void
471 dkim_errx(struct dkim_session *session, char *msg)
473 smtp_filter_disconnect(session->reqid, session->token,
474 "Internal server error");
475 log_warnx("%s", msg);
476 dkim_session_free(session);
479 void
480 dkim_parse_header(struct dkim_session *session, char *line, int force)
482 size_t i;
483 size_t r, w;
484 size_t linelen;
485 size_t lastheader;
486 size_t hlen;
487 int fieldname = 0;
488 char **mtmp;
489 char *htmp;
490 char *tmp;
492 if ((line[0] == ' ' || line[0] == '\t') && !session->lastheader)
493 return;
494 if ((line[0] != ' ' && line[0] != '\t')) {
495 session->lastheader = 0;
496 for (i = 0; i < nsign_headers; i++) {
497 hlen = strlen(sign_headers[i]);
498 if (strncasecmp(line, sign_headers[i], hlen) == 0) {
499 while (line[hlen] == ' ' || line[hlen] == '\t')
500 hlen++;
501 if (line[hlen] != ':')
502 continue;
503 break;
506 if (i == nsign_headers && !force)
507 return;
510 if (canonheader == CANON_RELAXED) {
511 if (!session->lastheader)
512 fieldname = 1;
513 for (r = w = 0; line[r] != '\0'; r++) {
514 if (line[r] == ':' && fieldname) {
515 if (line[w - 1] == ' ')
516 line[w - 1] = ':';
517 else
518 line[w++] = ':';
519 fieldname = 0;
520 while (line[r + 1] == ' ' ||
521 line[r + 1] == '\t')
522 r++;
523 continue;
525 if (line[r] == ' ' || line[r] == '\t' ||
526 line[r] == '\r' || line[r] == '\n') {
527 if (r != 0 && line[w - 1] == ' ')
528 continue;
529 else
530 line[w++] = ' ';
531 } else if (fieldname) {
532 line[w++] = tolower(line[r]);
533 continue;
534 } else
535 line[w++] = line[r];
537 linelen = line[w - 1] == ' ' ? w - 1 : w;
538 line[linelen] = '\0';
539 } else
540 linelen = strlen(line);
542 for (lastheader = 0; session->headers[lastheader] != NULL; lastheader++)
543 continue;
544 if (!session->lastheader) {
545 mtmp = recallocarray(session->headers, lastheader + 1,
546 lastheader + 2, sizeof(*mtmp));
547 if (mtmp == NULL) {
548 dkim_err(session, "Can't store header");
549 return;
551 session->headers = mtmp;
553 session->headers[lastheader] = strdup(line);
554 session->headers[lastheader + 1 ] = NULL;
555 session->lastheader = 1;
556 } else {
557 lastheader--;
558 linelen += strlen(session->headers[lastheader]);
559 if (canonheader == CANON_SIMPLE)
560 linelen += 2;
561 linelen++;
562 htmp = reallocarray(session->headers[lastheader], linelen,
563 sizeof(*htmp));
564 if (htmp == NULL) {
565 dkim_err(session, "Can't store header");
566 return;
568 session->headers[lastheader] = htmp;
569 if (canonheader == CANON_SIMPLE) {
570 if (strlcat(htmp, "\r\n", linelen) >= linelen)
571 fatalx("Missized header");
572 } else if (canonheader == CANON_RELAXED &&
573 (tmp = strchr(session->headers[lastheader], ':')) != NULL &&
574 tmp[1] == '\0')
575 line++;
577 if (strlcat(htmp, line, linelen) >= linelen)
578 fatalx("Missized header");
582 void
583 dkim_parse_body(struct dkim_session *session, char *line)
585 size_t r, w;
586 size_t linelen;
588 if (canonbody == CANON_RELAXED) {
589 for (r = w = 0; line[r] != '\0'; r++) {
590 if (line[r] == ' ' || line[r] == '\t') {
591 if (r != 0 && line[w - 1] == ' ')
592 continue;
593 else
594 line[w++] = ' ';
595 } else
596 line[w++] = line[r];
598 linelen = line[w - 1] == ' ' ? w - 1 : w;
599 line[linelen] = '\0';
600 } else
601 linelen = strlen(line);
602 for (; line[linelen - 1] == '\r'; linelen--)
603 continue;
604 line[linelen] = '\0';
606 if (line[0] == '\0') {
607 session->body_whitelines++;
608 return;
611 while (session->body_whitelines--) {
612 if (EVP_DigestUpdate(session->bh, "\r\n", 2) == 0) {
613 dkim_err(session, "Can't update hash context");
614 return;
617 session->body_whitelines = 0;
618 session->has_body = 1;
620 if (EVP_DigestUpdate(session->bh, line, linelen) == 0 ||
621 EVP_DigestUpdate(session->bh, "\r\n", 2) == 0) {
622 dkim_err(session, "Can't update hash context");
623 return;
627 int
628 dkim_signature_normalize(struct dkim_session *session)
630 size_t i;
631 size_t linelen;
632 size_t checkpoint;
633 size_t skip;
634 size_t *headerlen = &(session->signature.len);
635 int headername = 1;
636 char tag = '\0';
637 char *sig = session->signature.signature;
639 for (linelen = i = 0; sig[i] != '\0'; i++) {
640 if (sig[i] == '\r' && sig[i + 1] == '\n') {
641 i++;
642 checkpoint = 0;
643 linelen = 0;
644 continue;
646 if (sig[i] == '\t')
647 linelen = (linelen + 8) & ~7;
648 else
649 linelen++;
650 if (headername) {
651 if (sig[i] == ':') {
652 headername = 0;
653 checkpoint = i;
655 continue;
657 if (linelen > DKIM_SIGNATURE_LINELEN && checkpoint != 0) {
658 for (skip = checkpoint + 1;
659 sig[skip] == ' ' || sig[skip] == '\t';
660 skip++)
661 continue;
662 skip -= checkpoint + 1;
663 if (!dkim_signature_need(session,
664 skip > 3 ? 0 : 3 - skip + 1))
665 return 0;
666 sig = session->signature.signature;
668 memmove(sig + checkpoint + 3,
669 sig + checkpoint + skip,
670 *headerlen - skip - checkpoint + 1);
671 sig[checkpoint + 1] = '\r';
672 sig[checkpoint + 2] = '\n';
673 sig[checkpoint + 3] = '\t';
674 linelen = 8;
675 *headerlen = *headerlen + 3 - skip;
676 i = checkpoint + 3;
677 checkpoint = 0;
679 if (sig[i] == ';') {
680 checkpoint = i;
681 tag = '\0';
682 continue;
684 switch (tag) {
685 case 'B':
686 case 'b':
687 checkpoint = i;
688 break;
689 case 'h':
690 if (sig[i] == ':')
691 checkpoint = i;
693 if (tag == '\0' && sig[i] != ' ' && sig[i] != '\t') {
694 if ((tag = sig[i]) == 'b' && sig[i + 1] == 'h' &&
695 sig[i + 2] == '=') {
696 tag = 'B';
697 linelen += 2;
698 i += 2;
699 } else
700 tag = sig[i];
703 return 1;
706 int
707 dkim_signature_printf(struct dkim_session *session, char *fmt, ...)
709 struct dkim_signature *sig = &(session->signature);
710 va_list ap;
711 size_t newlen;
712 char *tmp;
713 size_t len;
715 va_start(ap, fmt);
716 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
717 fmt, ap)) >= sig->size - sig->len) {
718 va_end(ap);
719 if (!dkim_signature_need(session, len + 1))
720 return 0;
721 va_start(ap, fmt);
722 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
723 fmt, ap)) >= sig->size - sig->len)
724 fatalx("Miscalculated header size");
726 sig->len += len;
727 va_end(ap);
728 return 1;
731 int
732 dkim_signature_need(struct dkim_session *session, size_t len)
734 struct dkim_signature *sig = &(session->signature);
735 char *tmp;
737 if (sig->len + len < sig->size)
738 return 1;
739 sig->size = (((len + sig->len) / 512) + 1) * 512;
740 if ((tmp = realloc(sig->signature, sig->size)) == NULL) {
741 dkim_err(session, "No room for signature");
742 return 0;
744 sig->signature = tmp;
745 return 1;
748 __dead void
749 usage(void)
751 fprintf(stderr, "usage: %s [-a signalg] [-c canonicalization] [-h headerfields] -d domain -k keyfile "
752 "-s selector\n", getprogname());
753 exit(1);
756 RB_GENERATE(dkim_sessions, dkim_session, entry, dkim_session_cmp);