2 * Copyright (c) 2019 Martijn van Duren <martijn@openbsd.org>
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.
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.
18 #include <openssl/evp.h>
19 #include <openssl/pem.h>
20 #include <openssl/sha.h>
33 #include "smtp_proc.h"
35 struct dkim_signature {
48 size_t body_whitelines;
50 struct dkim_signature signature;
54 RB_ENTRY(dkim_session) entry;
57 RB_HEAD(dkim_sessions, dkim_session) dkim_sessions = RB_INITIALIZER(NULL);
58 RB_PROTOTYPE(dkim_sessions, dkim_session, entry, dkim_session_cmp);
60 /* RFC 6376 section 5.4.1 */
61 static char *dsign_headers[] = {
82 static char **sign_headers = dsign_headers;
83 static size_t nsign_headers = sizeof(dsign_headers) / sizeof(*dsign_headers);
85 static char *hashalg = "sha256";
86 static char *cryptalg = "rsa";
88 #define CANON_SIMPLE 0
89 #define CANON_RELAXED 1
90 static int canonheader = CANON_SIMPLE;
91 static int canonbody = CANON_SIMPLE;
93 static int addtime = 0;
94 static long long addexpire = 0;
95 static int addheaders = 0;
97 static char *domain = NULL;
98 static char *selector = NULL;
100 static EVP_PKEY *pkey;
101 static const EVP_MD *hash_md;
103 #define DKIM_SIGNATURE_LINELEN 78
106 void dkim_err(struct dkim_session *, char *);
107 void dkim_errx(struct dkim_session *, char *);
108 void dkim_headers_set(char *);
109 void dkim_dataline(char *, int, struct timespec *, char *, char *, uint64_t,
111 void dkim_commit(char *, int, struct timespec *, char *, char *, uint64_t,
113 void dkim_disconnect(char *, int, struct timespec *, char *, char *, uint64_t);
114 struct dkim_session *dkim_session_new(uint64_t);
115 void dkim_session_free(struct dkim_session *);
116 int dkim_session_cmp(struct dkim_session *, struct dkim_session *);
117 void dkim_parse_header(struct dkim_session *, char *, int);
118 void dkim_parse_body(struct dkim_session *, char *);
119 void dkim_sign(struct dkim_session *);
120 int dkim_signature_printheader(struct dkim_session *, char *);
121 int dkim_signature_printf(struct dkim_session *, char *, ...)
122 __attribute__((__format__ (printf, 2, 3)));
123 int dkim_signature_normalize(struct dkim_session *);
124 int dkim_signature_need(struct dkim_session *, size_t);
125 int dkim_sign_init(struct dkim_session *);
128 main(int argc, char *argv[])
136 while ((ch = getopt(argc, argv, "a:c:Dd:h:k:s:tx:zZ")) != -1) {
139 if (strncmp(optarg, "rsa-", 4) != 0)
140 err(1, "invalid algorithm");
141 hashalg = optarg + 4;
144 if (strncmp(optarg, "simple", 6) == 0) {
145 canonheader = CANON_SIMPLE;
147 } else if (strncmp(optarg, "relaxed", 7) == 0) {
148 canonheader = CANON_RELAXED;
151 err(1, "Invalid canonicalization");
152 if (optarg[0] == '/') {
153 if (strcmp(optarg + 1, "simple") == 0)
154 canonbody = CANON_SIMPLE;
155 else if (strcmp(optarg + 1, "relaxed") == 0)
156 canonbody = CANON_RELAXED;
158 err(1, "Invalid canonicalization");
159 } else if (optarg[0] == '\0')
160 canonbody = CANON_SIMPLE;
162 err(1, "Invalid canonicalization");
168 dkim_headers_set(optarg);
171 if ((keyfile = fopen(optarg, "r")) == NULL)
172 err(1, "Can't open key file");
173 pkey = PEM_read_PrivateKey(keyfile, NULL, NULL, NULL);
175 errx(1, "Can't read key file");
176 if (EVP_PKEY_get0_RSA(pkey) == NULL)
177 err(1, "Key is not of type rsa");
187 addexpire = strtonum(optarg, 1, INT64_MAX, &errstr);
189 errx(1, "Expire offset is %s", errstr);
205 OpenSSL_add_all_digests();
206 if ((hash_md = EVP_get_digestbyname(hashalg)) == NULL)
207 errx(1, "Can't find hash: %s", hashalg);
210 * fattr required for tmpfile.
211 * Can hopefully be removed in the future
213 if (pledge("fattr tmppath stdio", NULL) == -1)
216 if (domain == NULL || selector == NULL || pkey == NULL)
219 smtp_register_filter_dataline(dkim_dataline);
220 smtp_register_filter_commit(dkim_commit);
221 smtp_in_register_report_disconnect(dkim_disconnect);
228 dkim_disconnect(char *type, int version, struct timespec *tm, char *direction,
229 char *phase, uint64_t reqid)
231 struct dkim_session *session, search;
233 search.reqid = reqid;
234 if ((session = RB_FIND(dkim_sessions, &dkim_sessions, &search)) != NULL)
235 dkim_session_free(session);
239 dkim_dataline(char *type, int version, struct timespec *tm, char *direction,
240 char *phase, uint64_t reqid, uint64_t token, char *line)
242 struct dkim_session *session, search;
245 search.reqid = reqid;
246 session = RB_FIND(dkim_sessions, &dkim_sessions, &search);
247 if (session == NULL) {
248 if ((session = dkim_session_new(reqid)) == NULL)
250 session->token = token;
251 } else if (session->token != token)
252 errx(1, "Token incorrect");
256 linelen = strlen(line);
257 if (fprintf(session->origf, "%s\n", line) < linelen)
258 dkim_err(session, "Couldn't write to tempfile");
260 if (line[0] == '.' && line[1] =='\0') {
262 } else if (linelen != 0 && session->parsing_headers) {
265 dkim_parse_header(session, line, 0);
266 } else if (linelen == 0 && session->parsing_headers) {
267 if (addheaders > 0 && !dkim_signature_printf(session, "; "))
269 session->parsing_headers = 0;
273 dkim_parse_body(session, line);
278 dkim_commit(char *type, int version, struct timespec *tm, char *direction,
279 char *phase, uint64_t reqid, uint64_t token)
281 struct dkim_session *session, search;
283 search.reqid = reqid;
284 if ((session = RB_FIND(dkim_sessions, &dkim_sessions, &search)) == NULL)
285 errx(1, "Commit on undefined session");
288 smtp_filter_disconnect(session->reqid, session->token,
289 "Internal server error");
291 smtp_filter_proceed(reqid, token);
293 dkim_session_free(session);
296 struct dkim_session *
297 dkim_session_new(uint64_t reqid)
299 struct dkim_session *session;
300 struct dkim_signature *signature;
302 if ((session = calloc(1, sizeof(*session))) == NULL)
305 session->reqid = reqid;
306 if ((session->origf = tmpfile()) == NULL) {
307 dkim_err(session, "Can't open tempfile");
310 session->parsing_headers = 1;
312 session->body_whitelines = 0;
313 session->headers = calloc(1, sizeof(*(session->headers)));
314 if (session->headers == NULL) {
315 dkim_err(session, "Can't save headers");
318 session->lastheader = 0;
319 session->signature.signature = NULL;
320 session->signature.size = 0;
321 session->signature.len = 0;
324 if (!dkim_signature_printf(session,
325 "DKIM-signature: v=%s; a=%s-%s; c=%s/%s; d=%s; s=%s; ", "1",
327 canonheader == CANON_SIMPLE ? "simple" : "relaxed",
328 canonbody == CANON_SIMPLE ? "simple" : "relaxed",
331 if (addheaders > 0 && !dkim_signature_printf(session, "z="))
334 if ((session->b = EVP_MD_CTX_new()) == NULL ||
335 (session->bh = EVP_MD_CTX_new()) == NULL) {
336 dkim_errx(session, "Can't create hash context");
339 if (EVP_DigestSignInit(session->b, NULL, hash_md, NULL, pkey) <= 0 ||
340 EVP_DigestInit_ex(session->bh, hash_md, NULL) == 0) {
341 dkim_errx(session, "Failed to initialize hash context");
344 if (RB_INSERT(dkim_sessions, &dkim_sessions, session) != NULL)
345 errx(1, "session already registered");
350 dkim_session_free(struct dkim_session *session)
354 RB_REMOVE(dkim_sessions, &dkim_sessions, session);
355 fclose(session->origf);
356 EVP_MD_CTX_free(session->b);
357 EVP_MD_CTX_free(session->bh);
358 free(session->signature.signature);
359 for (i = 0; session->headers[i] != NULL; i++)
360 free(session->headers[i]);
361 free(session->headers);
366 dkim_session_cmp(struct dkim_session *s1, struct dkim_session *s2)
368 return (s1->reqid < s2->reqid ? -1 : s1->reqid > s2->reqid);
372 dkim_headers_set(char *headers)
379 for (i = 0; headers[i] != '\0'; i++) {
380 /* RFC 5322 field-name */
381 if (!(headers[i] >= 33 && headers[i] <= 126))
382 errx(1, "-h: invalid character");
383 if (headers[i] == ':') {
384 /* Test for empty headers */
385 if (i == 0 || headers[i - 1] == ':')
386 errx(1, "-h: header can't be empty");
389 headers[i] = tolower(headers[i]);
391 if (headers[i - 1] == ':')
392 errx(1, "-h: header can't be empty");
394 sign_headers = reallocarray(NULL, nsign_headers + 1, sizeof(*sign_headers));
395 if (sign_headers == NULL)
398 for (i = 0; i < nsign_headers; i++) {
399 sign_headers[i] = headers;
400 if (i != nsign_headers - 1) {
401 headers = strchr(headers, ':');
404 if (strcasecmp(sign_headers[i], "from") == 0)
408 errx(1, "From header must be included");
412 dkim_err(struct dkim_session *session, char *msg)
419 dkim_errx(struct dkim_session *session, char *msg)
426 dkim_parse_header(struct dkim_session *session, char *line, int force)
438 if (addheaders == 2 && !force &&
439 !dkim_signature_printheader(session, line))
442 if ((line[0] == ' ' || line[0] == '\t') && !session->lastheader)
444 if ((line[0] != ' ' && line[0] != '\t')) {
445 session->lastheader = 0;
446 for (i = 0; i < nsign_headers; i++) {
447 hlen = strlen(sign_headers[i]);
448 if (strncasecmp(line, sign_headers[i], hlen) == 0) {
449 while (line[hlen] == ' ' || line[hlen] == '\t')
451 if (line[hlen] != ':')
456 if (i == nsign_headers && !force)
460 if (addheaders == 1 && !force &&
461 !dkim_signature_printheader(session, line))
464 if (canonheader == CANON_RELAXED) {
465 if (!session->lastheader)
467 for (r = w = 0; line[r] != '\0'; r++) {
468 if (line[r] == ':' && fieldname) {
469 if (line[w - 1] == ' ')
474 while (line[r + 1] == ' ' ||
479 if (line[r] == ' ' || line[r] == '\t' ||
480 line[r] == '\r' || line[r] == '\n') {
481 if (r != 0 && line[w - 1] == ' ')
485 } else if (fieldname) {
486 line[w++] = tolower(line[r]);
491 linelen = line[w - 1] == ' ' ? w - 1 : w;
492 line[linelen] = '\0';
494 linelen = strlen(line);
496 for (lastheader = 0; session->headers[lastheader] != NULL; lastheader++)
498 if (!session->lastheader) {
499 mtmp = recallocarray(session->headers, lastheader + 1,
500 lastheader + 2, sizeof(*mtmp));
502 dkim_err(session, "Can't store header");
505 session->headers = mtmp;
507 session->headers[lastheader] = strdup(line);
508 session->headers[lastheader + 1 ] = NULL;
509 session->lastheader = 1;
512 linelen += strlen(session->headers[lastheader]);
513 if (canonheader == CANON_SIMPLE)
516 htmp = reallocarray(session->headers[lastheader], linelen,
519 dkim_err(session, "Can't store header");
522 session->headers[lastheader] = htmp;
523 if (canonheader == CANON_SIMPLE) {
524 if (strlcat(htmp, "\r\n", linelen) >= linelen)
525 errx(1, "Missized header");
526 } else if (canonheader == CANON_RELAXED &&
527 (tmp = strchr(session->headers[lastheader], ':')) != NULL &&
531 if (strlcat(htmp, line, linelen) >= linelen)
532 errx(1, "Missized header");
537 dkim_parse_body(struct dkim_session *session, char *line)
542 if (canonbody == CANON_RELAXED) {
543 for (r = w = 0; line[r] != '\0'; r++) {
544 if (line[r] == ' ' || line[r] == '\t') {
545 if (r != 0 && line[w - 1] == ' ')
552 linelen = line[w - 1] == ' ' ? w - 1 : w;
553 line[linelen] = '\0';
555 linelen = strlen(line);
557 if (line[0] == '\0') {
558 session->body_whitelines++;
562 while (session->body_whitelines--) {
563 if (EVP_DigestUpdate(session->bh, "\r\n", 2) == 0) {
564 dkim_err(session, "Can't update hash context");
568 session->body_whitelines = 0;
569 session->has_body = 1;
571 if (EVP_DigestUpdate(session->bh, line, linelen) == 0 ||
572 EVP_DigestUpdate(session->bh, "\r\n", 2) == 0) {
573 dkim_err(session, "Can't update hash context");
579 dkim_sign(struct dkim_session *session)
581 /* Use largest hash size here */
582 char bbh[EVP_MAX_MD_SIZE];
583 char bh[(((sizeof(bbh) + 2) / 3) * 4) + 1];
591 if (addtime || addexpire)
593 if (addtime && !dkim_signature_printf(session, "t=%lld; ", now))
595 if (addexpire != 0 && !dkim_signature_printf(session, "x=%lld; ",
596 now + addexpire < now ? INT64_MAX : now + addexpire))
599 if (canonbody == CANON_SIMPLE && !session->has_body) {
600 if (EVP_DigestUpdate(session->bh, "\r\n", 2) <= 0) {
601 dkim_err(session, "Can't update hash context");
605 if (EVP_DigestFinal_ex(session->bh, bbh, NULL) == 0) {
606 dkim_err(session, "Can't finalize hash context");
609 EVP_EncodeBlock(bh, bbh, EVP_MD_CTX_size(session->bh));
610 if (!dkim_signature_printf(session, "bh=%s; h=", bh))
612 /* Reverse order for ease of use of RFC6367 section 5.4.2 */
613 for (i = 0; session->headers[i] != NULL; i++)
615 for (i--; i >= 0; i--) {
616 if (EVP_DigestSignUpdate(session->b,
618 strlen(session->headers[i])) <= 0 ||
619 EVP_DigestSignUpdate(session->b, "\r\n", 2) <= 0) {
620 dkim_errx(session, "Failed to update digest context");
623 /* We're done with the cached header after hashing */
624 for (tmp = session->headers[i]; tmp[0] != ':'; tmp++) {
625 if (tmp[0] == ' ' || tmp[0] == '\t')
627 tmp[0] = tolower(tmp[0]);
630 if (!dkim_signature_printf(session, "%s%s",
631 session->headers[i + 1] == NULL ? "" : ":",
632 session->headers[i]))
636 dkim_signature_printf(session, "; b=");
637 if (!dkim_signature_normalize(session))
639 if ((tmp = strdup(session->signature.signature)) == NULL) {
640 dkim_err(session, "Can't create DKIM signature");
643 dkim_parse_header(session, tmp, 1);
644 if (EVP_DigestSignUpdate(session->b, tmp, strlen(tmp)) <= 0) {
645 dkim_err(session, "Failed to update digest context");
649 if (EVP_DigestSignFinal(session->b, NULL, &linelen) <= 0) {
650 dkim_err(session, "Failed to finalize digest");
653 if ((tmp = malloc(linelen)) == NULL) {
654 dkim_err(session, "Can't allocate space for digest");
657 if (EVP_DigestSignFinal(session->b, tmp, &linelen) <= 0) {
658 dkim_err(session, "Failed to finalize digest");
661 if ((b = malloc((((linelen + 2) / 3) * 4) + 1)) == NULL) {
662 dkim_err(session, "Can't create DKIM signature");
665 EVP_EncodeBlock(b, tmp, linelen);
667 dkim_signature_printf(session, "%s\r\n", b);
669 dkim_signature_normalize(session);
670 tmp = session->signature.signature;
671 while ((tmp2 = strchr(tmp, '\r')) != NULL) {
673 smtp_filter_dataline(session->reqid, session->token,
679 rewind(session->origf);
680 while ((i = getline(&tmp, &linelen, session->origf)) != -1) {
682 smtp_filter_dataline(session->reqid, session->token, "%s", tmp);
687 dkim_signature_normalize(struct dkim_session *session)
693 size_t *headerlen = &(session->signature.len);
696 char *sig = session->signature.signature;
698 for (linelen = i = 0; sig[i] != '\0'; i++) {
699 if (sig[i] == '\r' && sig[i + 1] == '\n') {
706 linelen = (linelen + 8) & ~7;
716 if (linelen > DKIM_SIGNATURE_LINELEN && checkpoint != 0) {
717 for (skip = checkpoint + 1;
718 sig[skip] == ' ' || sig[skip] == '\t';
721 skip -= checkpoint + 1;
722 if (!dkim_signature_need(session,
723 skip > 3 ? 0 : 3 - skip + 1))
725 sig = session->signature.signature;
727 memmove(sig + checkpoint + 3,
728 sig + checkpoint + skip,
729 *headerlen - skip - checkpoint + 1);
730 sig[checkpoint + 1] = '\r';
731 sig[checkpoint + 2] = '\n';
732 sig[checkpoint + 3] = '\t';
734 *headerlen = *headerlen + 3 - skip;
754 if (tag == '\0' && sig[i] != ' ' && sig[i] != '\t') {
755 if ((tag = sig[i]) == 'b' && sig[i + 1] == 'h' &&
768 dkim_signature_printheader(struct dkim_session *session, char *header)
771 static char *fmtheader = NULL;
773 static size_t size = 0;
776 len = strlen(header);
777 if ((len + 3) * 3 < len) {
779 dkim_err(session, "Can't add z-component to header");
782 if ((len + 3) * 3 > size) {
783 if ((tmp = reallocarray(fmtheader, 3, len + 3)) == NULL) {
784 dkim_err(session, "Can't add z-component to header");
788 size = (len + 1) * 3;
791 first = session->signature.signature[session->signature.len - 1] == '=';
792 for (j = i = 0; header[i] != '\0'; i++, j++) {
793 if (i == 0 && header[i] != ' ' && header[i] != '\t' && !first)
794 fmtheader[j++] = '|';
795 if ((header[i] >= 0x21 && header[i] <= 0x3A) ||
797 (header[i] >= 0x3E && header[i] <= 0x7B) ||
798 (header[i] >= 0x7D && header[i] <= 0x7E))
799 fmtheader[j] = header[i];
801 fmtheader[j++] = '=';
802 (void) sprintf(fmtheader + j, "%02hhX", header[i]);
806 (void) sprintf(fmtheader + j, "=%02hhX=%02hhX", (unsigned char) '\r',
807 (unsigned char) '\n');
809 return dkim_signature_printf(session, "%s", fmtheader);
813 dkim_signature_printf(struct dkim_session *session, char *fmt, ...)
815 struct dkim_signature *sig = &(session->signature);
822 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
823 fmt, ap)) >= sig->size - sig->len) {
825 if (!dkim_signature_need(session, len + 1))
828 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
829 fmt, ap)) >= sig->size - sig->len)
830 errx(1, "Miscalculated header size");
838 dkim_signature_need(struct dkim_session *session, size_t len)
840 struct dkim_signature *sig = &(session->signature);
843 if (sig->len + len < sig->size)
845 sig->size = (((len + sig->len) / 512) + 1) * 512;
846 if ((tmp = realloc(sig->signature, sig->size)) == NULL) {
847 dkim_err(session, "No room for signature");
850 sig->signature = tmp;
857 fprintf(stderr, "usage: %s [-a signalg] [-c canonicalization] [-h headerfields] -d domain -k keyfile "
858 "-s selector\n", getprogname());
862 RB_GENERATE(dkim_sessions, dkim_session, entry, dkim_session_cmp);