/* * Copyright (c) 2019 Martijn van Duren * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "openbsd-compat.h" #include "opensmtpd.h" #include "mheader.h" struct dkim_signature { char *signature; size_t size; size_t len; }; struct dkim_message { FILE *origf; int parsing_headers; char **headers; int lastheader; size_t body_whitelines; int has_body; struct dkim_signature signature; EVP_MD_CTX *dctx; }; /* RFC 6376 section 5.4.1 */ static char *dsign_headers[] = { "from", "reply-to", "subject", "date", "to", "cc", "resent-date", "resent-from", "resent-to", "resent-cc", "in-reply-to", "references", "list-id", "list-help", "list-unsubscribe", "list-subscribe", "list-post", "list-owner", "list-archive" }; static char **sign_headers = dsign_headers; static size_t nsign_headers = sizeof(dsign_headers) / sizeof(*dsign_headers); static char *hashalg = "sha256"; static char *cryptalg = "rsa"; #define CANON_SIMPLE 0 #define CANON_RELAXED 1 static int canonheader = CANON_SIMPLE; static int canonbody = CANON_SIMPLE; static int addtime = 0; static long long addexpire = 0; static int addheaders = 0; static char **domain = NULL; static size_t ndomains = 0; static char *selector = NULL; static EVP_PKEY *pkey; static const EVP_MD *hash_md; static int keyid = EVP_PKEY_RSA; static int sephash = 0; #define DKIM_SIGNATURE_LINELEN 78 void usage(void); void dkim_adddomain(char *); void dkim_headers_set(char *); void dkim_dataline(struct osmtpd_ctx *, const char *); void *dkim_message_new(struct osmtpd_ctx *); void dkim_message_free(struct osmtpd_ctx *, void *); void dkim_parse_header(struct dkim_message *, char *, int); void dkim_parse_body(struct dkim_message *, char *); void dkim_sign(struct osmtpd_ctx *); int dkim_signature_printheader(struct dkim_message *, const char *); int dkim_signature_printf(struct dkim_message *, char *, ...) __attribute__((__format__ (printf, 2, 3))); int dkim_signature_normalize(struct dkim_message *); const char *dkim_domain_select(struct dkim_message *, char *); int dkim_signature_need(struct dkim_message *, size_t); int dkim_sign_init(struct dkim_message *); int main(int argc, char *argv[]) { int ch; FILE *file; char *line; size_t linesz; ssize_t linelen; const char *errstr; while ((ch = getopt(argc, argv, "a:c:D:d:h:k:s:tx:z")) != -1) { switch (ch) { case 'a': if (strncmp(optarg, "rsa-", 4) == 0) { cryptalg = "rsa"; hashalg = optarg + 4; keyid = EVP_PKEY_RSA; sephash = 0; #ifdef HAVE_ED25519 } else if (strncmp(optarg, "ed25519-", 8) == 0) { hashalg = optarg + 8; cryptalg = "ed25519"; keyid = EVP_PKEY_ED25519; sephash = 1; #endif } else osmtpd_errx(1, "invalid algorithm"); break; case 'c': if (strncmp(optarg, "simple", 6) == 0) { canonheader = CANON_SIMPLE; optarg += 6; } else if (strncmp(optarg, "relaxed", 7) == 0) { canonheader = CANON_RELAXED; optarg += 7; } else osmtpd_errx(1, "Invalid canonicalization"); if (optarg[0] == '/') { if (strcmp(optarg + 1, "simple") == 0) canonbody = CANON_SIMPLE; else if (strcmp(optarg + 1, "relaxed") == 0) canonbody = CANON_RELAXED; else osmtpd_errx(1, "Invalid canonicalization"); } else if (optarg[0] == '\0') canonbody = CANON_SIMPLE; else osmtpd_errx(1, "Invalid canonicalization"); break; case 'D': if ((file = fopen(optarg, "r")) == NULL) osmtpd_err(1, "Can't open domain file (%s)", optarg); do { line = NULL; linesz = 0; linelen = getline(&line, &linesz, file); if (linelen > 0) { if (line[linelen - 1] == '\n') line[linelen - 1] = '\0'; if (line[0] == '#') continue; dkim_adddomain(line); } } while (linelen != -1); if (ferror(file)) osmtpd_err(1, "Error reading domain file (%s)", optarg); fclose(file); break; case 'd': dkim_adddomain(optarg); break; case 'h': dkim_headers_set(optarg); break; case 'k': if ((file = fopen(optarg, "r")) == NULL) osmtpd_err(1, "Can't open key file (%s)", optarg); pkey = PEM_read_PrivateKey(file, NULL, NULL, NULL); if (pkey == NULL) osmtpd_errx(1, "Can't read key file"); fclose(file); break; case 's': selector = optarg; break; case 't': addtime = 1; break; case 'x': addexpire = strtonum(optarg, 1, INT64_MAX, &errstr); if (addexpire == 0) osmtpd_errx(1, "Expire offset is %s", errstr); break; case 'z': addheaders++; break; default: usage(); } } OpenSSL_add_all_digests(); if (pledge("tmppath stdio", NULL) == -1) osmtpd_err(1, "pledge"); if ((hash_md = EVP_get_digestbyname(hashalg)) == NULL) osmtpd_errx(1, "Can't find hash: %s", hashalg); if (domain == NULL || selector == NULL || pkey == NULL) usage(); if (EVP_PKEY_id(pkey) != keyid) osmtpd_errx(1, "Key is not of type %s", cryptalg); osmtpd_register_filter_dataline(dkim_dataline); osmtpd_local_message(dkim_message_new, dkim_message_free); osmtpd_run(); return 0; } void dkim_adddomain(char *d) { domain = reallocarray(domain, ndomains + 1, sizeof(*domain)); if (domain == NULL) osmtpd_err(1, "%s: reallocarray", __func__); domain[ndomains++] = d; } void dkim_dataline(struct osmtpd_ctx *ctx, const char *line) { struct dkim_message *message = ctx->local_message; char *linedup; size_t linelen; linelen = strlen(line); if (fprintf(message->origf, "%s\n", line) < (int) linelen) osmtpd_errx(1, "Couldn't write to tempfile"); if (line[0] == '.' && line[1] =='\0') { dkim_sign(ctx); } else if (linelen != 0 && message->parsing_headers) { if (line[0] == '.') line++; if ((linedup = strdup(line)) == NULL) osmtpd_err(1, "%s: strdup", __func__); dkim_parse_header(message, linedup, 0); free(linedup); } else if (linelen == 0 && message->parsing_headers) { if (addheaders > 0 && !dkim_signature_printf(message, "; ")) return; message->parsing_headers = 0; } else { if (line[0] == '.') line++; if ((linedup = strdup(line)) == NULL) osmtpd_err(1, "%s: strdup", __func__); dkim_parse_body(message, linedup); free(linedup); } } void * dkim_message_new(struct osmtpd_ctx *ctx) { struct dkim_message *message; if ((message = calloc(1, sizeof(*message))) == NULL) { osmtpd_err(1, "%s: calloc", __func__); return NULL; } if ((message->origf = tmpfile()) == NULL) { osmtpd_warn(NULL, "Failed to open tempfile"); goto fail; } message->parsing_headers = 1; message->body_whitelines = 0; message->headers = calloc(1, sizeof(*(message->headers))); if (message->headers == NULL) osmtpd_err(1, "%s: calloc", __func__); message->lastheader = 0; message->signature.signature = NULL; message->signature.size = 0; message->signature.len = 0; if (!dkim_signature_printf(message, "DKIM-Signature: v=%s; a=%s-%s; c=%s/%s; s=%s; ", "1", cryptalg, hashalg, canonheader == CANON_SIMPLE ? "simple" : "relaxed", canonbody == CANON_SIMPLE ? "simple" : "relaxed", selector)) goto fail; if (addheaders > 0 && !dkim_signature_printf(message, "z=")) goto fail; if ((message->dctx = EVP_MD_CTX_new()) == NULL) osmtpd_errx(1, "EVP_MD_CTX_new"); if (EVP_DigestInit_ex(message->dctx, hash_md, NULL) <= 0) osmtpd_errx(1, "EVP_DigestInit_ex"); return message; fail: dkim_message_free(ctx, message); return NULL; } void dkim_message_free(struct osmtpd_ctx *ctx, void *data) { struct dkim_message *message = data; size_t i; fclose(message->origf); EVP_MD_CTX_free(message->dctx); free(message->signature.signature); for (i = 0; message->headers != NULL && message->headers[i] != NULL; i++) free(message->headers[i]); free(message->headers); free(message); } void dkim_headers_set(char *headers) { size_t i; int has_from = 0; nsign_headers = 1; for (i = 0; headers[i] != '\0'; i++) { /* RFC 5322 field-name */ if (!(headers[i] >= 33 && headers[i] <= 126)) osmtpd_errx(1, "-h: invalid character"); if (headers[i] == ':') { /* Test for empty headers */ if (i == 0 || headers[i - 1] == ':') osmtpd_errx(1, "-h: header can't be empty"); nsign_headers++; } headers[i] = tolower(headers[i]); } if (headers[i - 1] == ':') osmtpd_errx(1, "-h: header can't be empty"); if ((sign_headers = reallocarray(NULL, nsign_headers + 1, sizeof(*sign_headers))) == NULL) osmtpd_errx(1, "%s: reallocarray", __func__); for (i = 0; i < nsign_headers; i++) { sign_headers[i] = headers; if (i != nsign_headers - 1) { headers = strchr(headers, ':'); headers++[0] = '\0'; } if (strcasecmp(sign_headers[i], "from") == 0) has_from = 1; } if (!has_from) osmtpd_errx(1, "From header must be included"); } void dkim_parse_header(struct dkim_message *message, char *line, int force) { size_t i; size_t r, w; size_t linelen; size_t lastheader; size_t hlen; int fieldname = 0; char **mtmp; char *htmp; char *tmp; if (addheaders == 2 && !force && !dkim_signature_printheader(message, line)) return; if ((line[0] == ' ' || line[0] == '\t') && !message->lastheader) return; if ((line[0] != ' ' && line[0] != '\t')) { message->lastheader = 0; for (i = 0; i < nsign_headers; i++) { hlen = strlen(sign_headers[i]); if (strncasecmp(line, sign_headers[i], hlen) == 0) { while (line[hlen] == ' ' || line[hlen] == '\t') hlen++; if (line[hlen] != ':') continue; break; } } if (i == nsign_headers && !force) return; } if (addheaders == 1 && !force && !dkim_signature_printheader(message, line)) return; if (canonheader == CANON_RELAXED) { if (!message->lastheader) fieldname = 1; for (r = w = 0; line[r] != '\0'; r++) { if (line[r] == ':' && fieldname) { if (w > 0 && line[w - 1] == ' ') line[w - 1] = ':'; else line[w++] = ':'; fieldname = 0; while (line[r + 1] == ' ' || line[r + 1] == '\t') r++; continue; } if (line[r] == ' ' || line[r] == '\t' || line[r] == '\r' || line[r] == '\n') { if (r != 0 && w != 0 && line[w - 1] == ' ') continue; else line[w++] = ' '; } else if (fieldname) { line[w++] = tolower(line[r]); continue; } else line[w++] = line[r]; } linelen = (w != 0 && line[w - 1] == ' ') ? w - 1 : w; line[linelen] = '\0'; } else linelen = strlen(line); for (lastheader = 0; message->headers[lastheader] != NULL; lastheader++) continue; if (!message->lastheader) { mtmp = recallocarray(message->headers, lastheader + 1, lastheader + 2, sizeof(*mtmp)); if (mtmp == NULL) osmtpd_err(1, "%s: reallocarray", __func__); message->headers = mtmp; if ((message->headers[lastheader] = strdup(line)) == NULL) osmtpd_err(1, "%s: strdup", __func__); message->headers[lastheader + 1 ] = NULL; message->lastheader = 1; } else { lastheader--; linelen += strlen(message->headers[lastheader]); if (canonheader == CANON_SIMPLE) linelen += 2; linelen++; htmp = reallocarray(message->headers[lastheader], linelen, sizeof(*htmp)); if (htmp == NULL) osmtpd_err(1, "%s: reallocarray", __func__); message->headers[lastheader] = htmp; if (canonheader == CANON_SIMPLE) { if (strlcat(htmp, "\r\n", linelen) >= linelen) osmtpd_warnx(NULL, "Missized header"); } else if (canonheader == CANON_RELAXED && (tmp = strchr(message->headers[lastheader], ':')) != NULL && tmp[1] == '\0') line++; if (strlcat(htmp, line, linelen) >= linelen) osmtpd_warnx(NULL, "Missized header"); } } void dkim_parse_body(struct dkim_message *message, char *line) { size_t r, w; size_t linelen; if (canonbody == CANON_RELAXED) { for (r = w = 0; line[r] != '\0'; r++) { if (line[r] == ' ' || line[r] == '\t') { if (r != 0 && line[w - 1] == ' ') continue; else line[w++] = ' '; } else line[w++] = line[r]; } linelen = (w != 0 && line[w - 1] == ' ') ? w - 1 : w; line[linelen] = '\0'; } else linelen = strlen(line); if (line[0] == '\0') { message->body_whitelines++; return; } while (message->body_whitelines--) { if (EVP_DigestUpdate(message->dctx, "\r\n", 2) == 0) osmtpd_errx(1, "EVP_DigestUpdate"); } message->body_whitelines = 0; message->has_body = 1; if (EVP_DigestUpdate(message->dctx, line, linelen) == 0 || EVP_DigestUpdate(message->dctx, "\r\n", 2) == 0) osmtpd_errx(1, "EVP_DigestUpdate"); } void dkim_sign(struct osmtpd_ctx *ctx) { struct dkim_message *message = ctx->local_message; /* Use largest hash size here */ unsigned char bdigest[EVP_MAX_MD_SIZE]; unsigned char digest[(((sizeof(bdigest) + 2) / 3) * 4) + 1]; unsigned char *b; const char *sdomain = domain[0], *tsdomain; time_t now; ssize_t i; size_t linelen = 0; char *tmp, *tmp2; unsigned int digestsz; if (addtime || addexpire) now = time(NULL); if (addtime && !dkim_signature_printf(message, "t=%lld; ", (long long)now)) goto fail; if (addexpire != 0 && !dkim_signature_printf(message, "x=%lld; ", now + addexpire < now ? INT64_MAX : now + addexpire)) goto fail; if (canonbody == CANON_SIMPLE && !message->has_body) { if (EVP_DigestUpdate(message->dctx, "\r\n", 2) <= 0) osmtpd_errx(1, "EVP_DigestUpdate"); } if (EVP_DigestFinal_ex(message->dctx, bdigest, &digestsz) == 0) osmtpd_errx(1, "EVP_DigestFinal_ex"); EVP_EncodeBlock(digest, bdigest, digestsz); if (!dkim_signature_printf(message, "bh=%s; h=", digest)) goto fail; /* Reverse order for ease of use of RFC6367 section 5.4.2 */ for (i = 0; message->headers[i] != NULL; i++) continue; EVP_MD_CTX_reset(message->dctx); if (!sephash) { if (EVP_DigestSignInit(message->dctx, NULL, hash_md, NULL, pkey) != 1) osmtpd_errx(1, "EVP_DigestSignInit"); } else { if (EVP_DigestInit_ex(message->dctx, hash_md, NULL) != 1) osmtpd_errx(1, "EVP_DigestInit_ex"); } for (i--; i >= 0; i--) { if (!sephash) { if (EVP_DigestSignUpdate(message->dctx, message->headers[i], strlen(message->headers[i])) != 1 || EVP_DigestSignUpdate(message->dctx, "\r\n", 2) <= 0) osmtpd_errx(1, "EVP_DigestSignUpdate"); } else { if (EVP_DigestUpdate(message->dctx, message->headers[i], strlen(message->headers[i])) != 1 || EVP_DigestUpdate(message->dctx, "\r\n", 2) <= 0) osmtpd_errx(1, "EVP_DigestSignUpdate"); } if ((tsdomain = dkim_domain_select(message, message->headers[i])) != NULL) sdomain = tsdomain; /* We're done with the cached header after hashing */ for (tmp = message->headers[i]; tmp[0] != ':'; tmp++) { if (tmp[0] == ' ' || tmp[0] == '\t') break; tmp[0] = tolower(tmp[0]); } tmp[0] = '\0'; if (!dkim_signature_printf(message, "%s%s", message->headers[i + 1] == NULL ? "" : ":", message->headers[i])) goto fail; } dkim_signature_printf(message, "; d=%s; b=", sdomain); if (!dkim_signature_normalize(message)) goto fail; if ((tmp = strdup(message->signature.signature)) == NULL) osmtpd_err(1, "%s: strdup", __func__); dkim_parse_header(message, tmp, 1); if (!sephash) { if (EVP_DigestSignUpdate(message->dctx, tmp, strlen(tmp)) != 1) osmtpd_errx(1, "EVP_DigestSignUpdate"); } else { if (EVP_DigestUpdate(message->dctx, tmp, strlen(tmp)) != 1) osmtpd_errx(1, "EVP_DigestUpdate"); } free(tmp); if (!sephash) { if (EVP_DigestSignFinal(message->dctx, NULL, &linelen) != 1) osmtpd_errx(1, "EVP_DigestSignFinal"); #ifdef HAVE_ED25519 } else { if (EVP_DigestFinal_ex(message->dctx, bdigest, &digestsz) != 1) osmtpd_errx(1, "EVP_DigestFinal_ex"); EVP_MD_CTX_reset(message->dctx); if (EVP_DigestSignInit(message->dctx, NULL, NULL, NULL, pkey) != 1) osmtpd_errx(1, "EVP_DigestSignInit"); if (EVP_DigestSign(message->dctx, NULL, &linelen, bdigest, digestsz) != 1) osmtpd_errx(1, "EVP_DigestSign"); #endif } if ((tmp = malloc(linelen)) == NULL) osmtpd_err(1, "%s: malloc", __func__); if (!sephash) { if (EVP_DigestSignFinal(message->dctx, tmp, &linelen) != 1) osmtpd_errx(1, "EVP_DigestSignFinal"); #ifdef HAVE_ED25519 } else { if (EVP_DigestSign(message->dctx, tmp, &linelen, bdigest, digestsz) != 1) osmtpd_errx(1, "EVP_DigestSign"); #endif } if ((b = malloc((((linelen + 2) / 3) * 4) + 1)) == NULL) osmtpd_err(1, "%s: malloc", __func__); EVP_EncodeBlock(b, tmp, linelen); free(tmp); dkim_signature_printf(message, "%s\r\n", b); free(b); dkim_signature_normalize(message); tmp = message->signature.signature; while ((tmp2 = strchr(tmp, '\r')) != NULL) { tmp2[0] = '\0'; osmtpd_filter_dataline(ctx, "%s", tmp); tmp = tmp2 + 2; } tmp = NULL; linelen = 0; rewind(message->origf); while ((i = getline(&tmp, &linelen, message->origf)) != -1) { tmp[i - 1] = '\0'; osmtpd_filter_dataline(ctx, "%s", tmp); } free(tmp); return; fail: osmtpd_filter_dataline(ctx, "."); } int dkim_signature_normalize(struct dkim_message *message) { size_t i; size_t linelen; size_t checkpoint; size_t skip; size_t *headerlen = &(message->signature.len); int headername = 1; char tag = '\0'; char *sig = message->signature.signature; for (linelen = i = 0; sig[i] != '\0'; i++) { if (sig[i] == '\r' && sig[i + 1] == '\n') { i++; checkpoint = 0; linelen = 0; continue; } if (sig[i] == '\t') linelen = (linelen + 8) & ~7; else linelen++; if (headername) { if (sig[i] == ':') { headername = 0; checkpoint = i; } continue; } if (linelen > DKIM_SIGNATURE_LINELEN && checkpoint != 0) { for (skip = checkpoint + 1; sig[skip] == ' ' || sig[skip] == '\t'; skip++) continue; skip -= checkpoint + 1; if (!dkim_signature_need(message, skip > 3 ? 0 : 3 - skip + 1)) return 0; sig = message->signature.signature; memmove(sig + checkpoint + 3, sig + checkpoint + skip, *headerlen - skip - checkpoint + 1); sig[checkpoint + 1] = '\r'; sig[checkpoint + 2] = '\n'; sig[checkpoint + 3] = '\t'; linelen = 8; *headerlen = *headerlen + 3 - skip; i = checkpoint + 3; checkpoint = 0; } if (sig[i] == ';') { checkpoint = i; tag = '\0'; continue; } switch (tag) { case 'B': case 'b': case 'z': checkpoint = i; break; case 'h': if (sig[i] == ':') checkpoint = i; break; } if (tag == '\0' && sig[i] != ' ' && sig[i] != '\t') { if ((tag = sig[i]) == 'b' && sig[i + 1] == 'h' && sig[i + 2] == '=') { tag = 'B'; linelen += 2; i += 2; } else tag = sig[i]; } } return 1; } int dkim_signature_printheader(struct dkim_message *message, const char *line) { size_t i, j, len; int r; char *fmtheader; int first; len = strlen(line); if ((fmtheader = reallocarray(NULL, 3, len + 3)) == NULL) osmtpd_err(1, "malloc"); first = message->signature.signature[message->signature.len - 1] == '='; for (j = i = 0; line[i] != '\0'; i++, j++) { if (i == 0 && line[i] != ' ' && line[i] != '\t' && !first) fmtheader[j++] = '|'; if ((line[i] >= 0x21 && line[i] <= 0x3A) || (line[i] == 0x3C) || (line[i] >= 0x3E && line[i] <= 0x7B) || (line[i] >= 0x7D && line[i] <= 0x7E)) fmtheader[j] = line[i]; else { fmtheader[j++] = '='; (void) sprintf(fmtheader + j, "%02hhX", line[i]); j++; } } (void) sprintf(fmtheader + j, "=%02hhX=%02hhX", (unsigned char) '\r', (unsigned char) '\n'); r = dkim_signature_printf(message, "%s", fmtheader); free(fmtheader); return r; } int dkim_signature_printf(struct dkim_message *message, char *fmt, ...) { struct dkim_signature *sig = &(message->signature); va_list ap; size_t len; va_start(ap, fmt); if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len, fmt, ap)) >= sig->size - sig->len) { va_end(ap); if (!dkim_signature_need(message, len + 1)) return 0; va_start(ap, fmt); if ((len = vsnprintf(sig->signature + sig->len, sig->size - sig->len, fmt, ap)) >= sig->size - sig->len) osmtpd_errx(1, "Miscalculated header size"); } sig->len += len; va_end(ap); return 1; } const char * dkim_domain_select(struct dkim_message *message, char *from) { char *mdomain0, *mdomain; size_t i; if ((mdomain = mdomain0 = osmtpd_mheader_from_domain(from)) == NULL) return NULL; while (mdomain != NULL && mdomain[0] != '\0') { for (i = 0; i < ndomains; i++) { if (strcasecmp(mdomain, domain[i]) == 0) { free(mdomain0); return domain[i]; } } if ((mdomain = strchr(mdomain, '.')) != NULL) mdomain++; } free(mdomain0); return NULL; } int dkim_signature_need(struct dkim_message *message, size_t len) { struct dkim_signature *sig = &(message->signature); char *tmp; if (sig->len + len < sig->size) return 1; sig->size = (((len + sig->len) / 512) + 1) * 512; if ((tmp = realloc(sig->signature, sig->size)) == NULL) osmtpd_err(1, "%s: malloc", __func__); sig->signature = tmp; return 1; } __dead void usage(void) { fprintf(stderr, "usage: filter-dkimsign [-tz] [-a signalg] " "[-c canonicalization] \n [-h headerfields]" "[-x seconds] -D file -d domain -k keyfile -s selector\n"); exit(1); }