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;
53 RB_ENTRY(dkim_session) entry;
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[] = {
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
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,
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 *);
119 main(int argc, char *argv[])
126 while ((ch = getopt(argc, argv, "a:c:Dd:h:k:s:")) != -1) {
129 if (strncmp(optarg, "rsa-", 4) != 0)
130 err(1, "invalid algorithm");
131 hashalg = optarg + 4;
134 if (strncmp(optarg, "simple", 6) == 0) {
135 canonheader = CANON_SIMPLE;
137 } else if (strncmp(optarg, "relaxed", 7) == 0) {
138 canonheader = CANON_RELAXED;
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;
148 err(1, "Invalid canonicalization");
149 } else if (optarg[0] == '\0')
150 canonbody = CANON_SIMPLE;
152 err(1, "Invalid canonicalization");
158 dkim_headers_set(optarg);
161 if ((keyfile = fopen(optarg, "r")) == NULL)
162 err(1, "Can't open key file");
163 pkey = PEM_read_PrivateKey(keyfile, NULL, NULL, 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");
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)
189 if (domain == NULL || selector == NULL || pkey == NULL)
192 smtp_register_filter_dataline(dkim_dataline);
193 smtp_in_register_report_disconnect(dkim_disconnect);
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);
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];
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)
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");
246 if (EVP_DigestFinal_ex(session->bh, bbh, NULL) == 0) {
247 dkim_err(session, "Can't finalize hash context");
250 EVP_EncodeBlock(bh, bbh, EVP_MD_CTX_size(session->bh));
251 if (!dkim_signature_printf(session, "bh=%s; h=", bh))
253 /* Reverse order for ease of use of RFC6367 section 5.4.2 */
254 for (i = 0; session->headers[i] != NULL; i++)
256 for (i--; i >= 0; i--) {
257 if (EVP_DigestSignUpdate(session->b,
259 strlen(session->headers[i])) <= 0 ||
260 EVP_DigestSignUpdate(session->b,
263 "Failed to update digest context");
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')
270 tmp[0] = tolower(tmp[0]);
273 if (!dkim_signature_printf(session, "%s%s",
274 session->headers[i + 1] == NULL ? "" : ":",
275 session->headers[i]))
279 dkim_signature_printf(session, "; b=");
280 if (!dkim_signature_normalize(session))
282 if ((tmp = strdup(session->signature.signature)) == NULL) {
283 dkim_err(session, "Can't create DKIM signature");
286 dkim_parse_header(session, tmp, 1);
287 if (EVP_DigestSignUpdate(session->b, tmp,
289 dkim_err(session, "Failed to update digest context");
293 if (EVP_DigestSignFinal(session->b, NULL, &linelen) <= 0) {
294 dkim_err(session, "Failed to finalize digest");
297 if ((tmp = malloc(linelen)) == NULL) {
298 dkim_err(session, "Can't allocate space for digest");
301 if (EVP_DigestSignFinal(session->b, tmp, &linelen) <= 0) {
302 dkim_err(session, "Failed to finalize digest");
305 /* Lines are unlikely to overflow */
306 b = malloc((((linelen + 2) / 3) * 4) + 1);
307 EVP_EncodeBlock(b, tmp, linelen);
309 dkim_signature_printf(session, "%s\r\n", b);
311 dkim_signature_normalize(session);
312 tmp = session->signature.signature;
313 while ((tmp2 = strchr(tmp, '\r')) != NULL) {
315 smtp_filter_dataline(session->reqid, session->token,
321 rewind(session->origf);
322 while ((i = getline(&tmp, &linelen, session->origf)) != -1) {
324 smtp_filter_dataline(session->reqid, session->token,
327 dkim_session_free(session);
328 } else if (linelen != 0 && session->parsing_headers) {
331 dkim_parse_header(session, line, 0);
332 } else if (linelen == 0 && session->parsing_headers) {
333 session->parsing_headers = 0;
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";
349 if ((session = calloc(1, sizeof(*session))) == NULL)
352 session->reqid = reqid;
353 if ((fd = mkstemp(origfile)) == -1) {
354 dkim_err(session, "Can't open tempfile");
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");
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");
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",
379 canonheader == CANON_SIMPLE ? "simple" : "relaxed",
380 canonbody == CANON_SIMPLE ? "simple" : "relaxed",
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");
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");
394 if (RB_INSERT(dkim_sessions, &dkim_sessions, session) != NULL)
395 fatalx("session already registered");
400 dkim_session_free(struct dkim_session *session)
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);
416 dkim_session_cmp(struct dkim_session *s1, struct dkim_session *s2)
418 return (s1->reqid < s2->reqid ? -1 : s1->reqid > s2->reqid);
422 dkim_headers_set(char *headers)
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");
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)
448 for (i = 0; i < nsign_headers; i++) {
449 sign_headers[i] = headers;
450 if (i != nsign_headers - 1) {
451 headers = strchr(headers, ':');
454 if (strcasecmp(sign_headers[i], "from") == 0)
458 errx(1, "From header must be included");
462 dkim_err(struct dkim_session *session, char *msg)
464 smtp_filter_disconnect(session->reqid, session->token,
465 "Internal server error");
467 dkim_session_free(session);
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);
480 dkim_parse_header(struct dkim_session *session, char *line, int force)
492 if ((line[0] == ' ' || line[0] == '\t') && !session->lastheader)
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')
501 if (line[hlen] != ':')
506 if (i == nsign_headers && !force)
510 if (canonheader == CANON_RELAXED) {
511 if (!session->lastheader)
513 for (r = w = 0; line[r] != '\0'; r++) {
514 if (line[r] == ':' && fieldname) {
515 if (line[w - 1] == ' ')
520 while (line[r + 1] == ' ' ||
525 if (line[r] == ' ' || line[r] == '\t' ||
526 line[r] == '\r' || line[r] == '\n') {
527 if (r != 0 && line[w - 1] == ' ')
531 } else if (fieldname) {
532 line[w++] = tolower(line[r]);
537 linelen = line[w - 1] == ' ' ? w - 1 : w;
538 line[linelen] = '\0';
540 linelen = strlen(line);
542 for (lastheader = 0; session->headers[lastheader] != NULL; lastheader++)
544 if (!session->lastheader) {
545 mtmp = recallocarray(session->headers, lastheader + 1,
546 lastheader + 2, sizeof(*mtmp));
548 dkim_err(session, "Can't store header");
551 session->headers = mtmp;
553 session->headers[lastheader] = strdup(line);
554 session->headers[lastheader + 1 ] = NULL;
555 session->lastheader = 1;
558 linelen += strlen(session->headers[lastheader]);
559 if (canonheader == CANON_SIMPLE)
562 htmp = reallocarray(session->headers[lastheader], linelen,
565 dkim_err(session, "Can't store header");
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 &&
577 if (strlcat(htmp, line, linelen) >= linelen)
578 fatalx("Missized header");
583 dkim_parse_body(struct dkim_session *session, char *line)
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] == ' ')
598 linelen = line[w - 1] == ' ' ? w - 1 : w;
599 line[linelen] = '\0';
601 linelen = strlen(line);
602 for (; line[linelen - 1] == '\r'; linelen--)
604 line[linelen] = '\0';
606 if (line[0] == '\0') {
607 session->body_whitelines++;
611 while (session->body_whitelines--) {
612 if (EVP_DigestUpdate(session->bh, "\r\n", 2) == 0) {
613 dkim_err(session, "Can't update hash context");
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");
628 dkim_signature_normalize(struct dkim_session *session)
634 size_t *headerlen = &(session->signature.len);
637 char *sig = session->signature.signature;
639 for (linelen = i = 0; sig[i] != '\0'; i++) {
640 if (sig[i] == '\r' && sig[i + 1] == '\n') {
647 linelen = (linelen + 8) & ~7;
657 if (linelen > DKIM_SIGNATURE_LINELEN && checkpoint != 0) {
658 for (skip = checkpoint + 1;
659 sig[skip] == ' ' || sig[skip] == '\t';
662 skip -= checkpoint + 1;
663 if (!dkim_signature_need(session,
664 skip > 3 ? 0 : 3 - skip + 1))
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';
675 *headerlen = *headerlen + 3 - skip;
693 if (tag == '\0' && sig[i] != ' ' && sig[i] != '\t') {
694 if ((tag = sig[i]) == 'b' && sig[i + 1] == 'h' &&
707 dkim_signature_printf(struct dkim_session *session, char *fmt, ...)
709 struct dkim_signature *sig = &(session->signature);
716 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
717 fmt, ap)) >= sig->size - sig->len) {
719 if (!dkim_signature_need(session, len + 1))
722 if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len,
723 fmt, ap)) >= sig->size - sig->len)
724 fatalx("Miscalculated header size");
732 dkim_signature_need(struct dkim_session *session, size_t len)
734 struct dkim_signature *sig = &(session->signature);
737 if (sig->len + len < sig->size)
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");
744 sig->signature = tmp;
751 fprintf(stderr, "usage: %s [-a signalg] [-c canonicalization] [-h headerfields] -d domain -k keyfile "
752 "-s selector\n", getprogname());
756 RB_GENERATE(dkim_sessions, dkim_session, entry, dkim_session_cmp);