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/sha.h>
21 #include <ctype.h>
22 #include <err.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <syslog.h>
27 #include <time.h>
28 #include <unistd.h>
30 #include "log.h"
31 #include "smtp_proc.h"
33 struct dkim_session {
34 uint64_t reqid;
35 uint64_t token;
36 FILE *origf;
37 int parsing_headers;
38 char **headers;
39 int lastheader;
40 union {
41 SHA_CTX sha1;
42 SHA256_CTX sha256;
43 };
44 /* Use largest hash size her */
45 char bbh[SHA256_DIGEST_LENGTH];
46 char bh[(((SHA256_DIGEST_LENGTH + 2) / 3) * 4) + 1];
47 size_t body_whitelines;
48 int has_body;
49 RB_ENTRY(dkim_session) entry;
50 };
52 RB_HEAD(dkim_sessions, dkim_session) dkim_sessions = RB_INITIALIZER(NULL);
53 RB_PROTOTYPE(dkim_sessions, dkim_session, entry, dkim_session_cmp);
55 static char **sign_headers = NULL;
56 static size_t nsign_headers = 0;
58 #define HASH_SHA1 0
59 #define HASH_SHA256 1
60 static int hashalg = HASH_SHA256;
62 #define CRYPT_RSA 0
63 static int cryptalg = CRYPT_RSA;
65 #define CANON_SIMPLE 0
66 #define CANON_RELAXED 1
67 static int canonheader = CANON_SIMPLE;
68 static int canonbody = CANON_SIMPLE;
70 static char *domain = NULL;
72 void usage(void);
73 void dkim_err(struct dkim_session *, char *);
74 void dkim_errx(struct dkim_session *, char *);
75 void dkim_headers_set(char *);
76 void dkim_dataline(char *, int, struct timespec *, char *, char *, uint64_t,
77 uint64_t, char *);
78 void dkim_disconnect(char *, int, struct timespec *, char *, char *, uint64_t);
79 struct dkim_session *dkim_session_new(uint64_t);
80 void dkim_session_free(struct dkim_session *);
81 int dkim_session_cmp(struct dkim_session *, struct dkim_session *);
82 void dkim_parse_header(struct dkim_session *, char *);
83 void dkim_parse_body(struct dkim_session *, char *);
84 void dkim_built_signature(struct dkim_session *);
85 int dkim_hash_update(struct dkim_session *, char *, size_t);
86 int dkim_hash_final(struct dkim_session *, char *);
88 int
89 main(int argc, char *argv[])
90 {
91 int ch;
92 int i;
93 int debug = 0;
95 while ((ch = getopt(argc, argv, "a:c:Dd:h:")) != -1) {
96 switch (ch) {
97 case 'a':
98 if (strncmp(optarg, "rsa-", 4))
99 err(1, "invalid algorithm");
100 if (strcmp(optarg + 4, "sha256") == 0)
101 hashalg = HASH_SHA256;
102 else if (strcmp(optarg + 4, "sha1") == 0)
103 hashalg = HASH_SHA1;
104 else
105 err(1, "invalid algorithm");
106 break;
107 case 'c':
108 if (strncmp(optarg, "simple", 6) == 0) {
109 canonheader = CANON_SIMPLE;
110 optarg += 6;
111 } else if (strncmp(optarg, "relaxed", 7) == 0) {
112 canonheader = CANON_RELAXED;
113 optarg += 7;
114 } else
115 err(1, "Invalid canonicalization");
116 if (optarg[0] == '/') {
117 if (strcmp(optarg + 1, "simple") == 0)
118 canonbody = CANON_SIMPLE;
119 else if (strcmp(optarg + 1, "relaxed") == 0)
120 canonbody = CANON_RELAXED;
121 else
122 err(1, "Invalid canonicalization");
123 } else if (optarg[0] == '\0')
124 canonbody = CANON_SIMPLE;
125 else
126 err(1, "Invalid canonicalization");
127 break;
128 case 'd':
129 domain = optarg;
130 if (strlen(domain) > 255)
131 err(1, "Domain too long");
132 break;
133 case 'D':
134 debug = 1;
135 break;
136 case 'h':
137 dkim_headers_set(optarg);
138 break;
139 default:
140 usage();
144 log_init(debug, LOG_MAIL);
145 if (pledge("tmppath stdio", NULL) == -1)
146 fatal("pledge");
148 if (domain == NULL)
149 usage();
151 smtp_register_filter_dataline(dkim_dataline);
152 smtp_in_register_report_disconnect(dkim_disconnect);
153 smtp_run(debug);
155 return 0;
158 void
159 dkim_disconnect(char *type, int version, struct timespec *tm, char *direction,
160 char *phase, uint64_t reqid)
162 struct dkim_session *session, search;
164 search.reqid = reqid;
165 if ((session = RB_FIND(dkim_sessions, &dkim_sessions, &search)) != NULL)
166 dkim_session_free(session);
169 void
170 dkim_dataline(char *type, int version, struct timespec *tm, char *direction,
171 char *phase, uint64_t reqid, uint64_t token, char *line)
173 struct dkim_session *session, search;
174 size_t i;
175 size_t linelen;
177 search.reqid = reqid;
178 session = RB_FIND(dkim_sessions, &dkim_sessions, &search);
179 if (session == NULL) {
180 session = dkim_session_new(reqid);
181 session->token = token;
182 } else if (session->token != token)
183 fatalx("Token incorrect");
185 linelen = strlen(line);
186 if (fwrite(line, 1, linelen, session->origf) < linelen)
187 dkim_err(session, "Couldn't write to tempfile");
189 if (linelen != 0 && session->parsing_headers) {
190 dkim_parse_header(session, line);
191 } else if (linelen == 0 && session->parsing_headers) {
192 session->parsing_headers = 0;
193 } else if (line[0] == '.' && line[1] =='\0') {
194 if (canonbody == CANON_SIMPLE && !session->has_body) {
195 if (dkim_hash_update(session, "\r\n", 2) == 0)
196 return;
198 if (dkim_hash_final(session, session->bbh) == 0)
199 return;
200 EVP_EncodeBlock(session->bh, session->bbh,
201 hashalg == HASH_SHA1 ? SHA_DIGEST_LENGTH :
202 SHA256_DIGEST_LENGTH);
203 dkim_built_signature(session);
204 } else
205 dkim_parse_body(session, line);
208 struct dkim_session *
209 dkim_session_new(uint64_t reqid)
211 struct dkim_session *session;
212 char origfile[] = "/tmp/filter-dkimXXXXXX";
213 int fd;
215 if ((session = calloc(1, sizeof(*session))) == NULL)
216 fatal(NULL);
218 session->reqid = reqid;
219 if ((fd = mkstemp(origfile)) == -1) {
220 dkim_err(session, "Can't open tempfile");
221 return NULL;
223 if (unlink(origfile) == -1)
224 log_warn("Failed to unlink tempfile %s", origfile);
225 if ((session->origf = fdopen(fd, "r+")) == NULL) {
226 dkim_err(session, "Can't open tempfile");
227 return NULL;
229 session->parsing_headers = 1;
231 if (hashalg == HASH_SHA1)
232 SHA1_Init(&(session->sha1));
233 else
234 SHA256_Init(&(session->sha256));
235 session->body_whitelines = 0;
236 session->headers = calloc(1, sizeof(*(session->headers)));
237 if (session->headers == NULL) {
238 dkim_err(session, "Can't save headers");
239 return NULL;
241 session->lastheader = 0;
243 if (RB_INSERT(dkim_sessions, &dkim_sessions, session) != NULL)
244 fatalx("session already registered");
245 return session;
248 void
249 dkim_session_free(struct dkim_session *session)
251 size_t i;
253 RB_REMOVE(dkim_sessions, &dkim_sessions, session);
254 fclose(session->origf);
255 for (i = 0; session->headers[i] != NULL; i++)
256 free(session->headers[i]);
257 free(session->headers);
258 free(session);
261 int
262 dkim_session_cmp(struct dkim_session *s1, struct dkim_session *s2)
264 return (s1->reqid < s2->reqid ? -1 : s1->reqid > s2->reqid);
267 void
268 dkim_headers_set(char *headers)
270 size_t i;
271 int has_from = 0;
273 nsign_headers = 1;
275 /* We don't support FWS for the -h flag */
276 for (i = 0; headers[i] != '\0'; i++) {
277 /* RFC 5322 field-name */
278 if (!(headers[i] >= 33 && headers[i] <= 126))
279 errx(1, "-h: invalid character");
280 if (headers[i] == ':') {
281 /* Test for empty headers */
282 if (i == 0 || headers[i - 1] == ':')
283 errx(1, "-h: header can't be empty");
284 nsign_headers++;
286 headers[i] = tolower(headers[i]);
288 if (headers[i - 1] == ':')
289 errx(1, "-h: header can't be empty");
291 sign_headers = reallocarray(NULL, nsign_headers, sizeof(*sign_headers));
292 if (sign_headers == NULL)
293 errx(1, NULL);
295 for (i = 0; i < nsign_headers; i++) {
296 sign_headers[i] = headers;
297 if (i != nsign_headers - 1) {
298 headers = strchr(headers, ':');
299 headers++[0] = '\0';
301 if (strcasecmp(sign_headers[i], "from") == 0)
302 has_from = 1;
304 if (!has_from)
305 errx(1, "From header must be included");
308 void
309 dkim_err(struct dkim_session *session, char *msg)
311 smtp_filter_disconnect(session->reqid, session->token,
312 "Internal server error");
313 log_warn("%s", msg);
314 dkim_session_free(session);
317 void
318 dkim_errx(struct dkim_session *session, char *msg)
320 smtp_filter_disconnect(session->reqid, session->token,
321 "Internal server error");
322 log_warnx("%s", msg);
323 dkim_session_free(session);
326 void
327 dkim_parse_header(struct dkim_session *session, char *line)
329 size_t i;
330 size_t r, w;
331 size_t linelen;
332 size_t lastheader;
333 int fieldname;
334 char **mtmp;
335 char *htmp;
337 if ((line[0] == ' ' || line[0] == '\t') && !session->lastheader)
338 return;
339 if ((line[0] != ' ' && line[0] != '\t')) {
340 for (i = 0; i < nsign_headers; i++) {
341 if (strncasecmp(line, sign_headers[i],
342 strlen(sign_headers[i])) == 0) {
343 break;
346 if (i == nsign_headers) {
347 session->lastheader = 0;
348 return;
352 if (canonheader == CANON_RELAXED) {
353 fieldname = 1;
354 for (r = w = 0; line[r] != '\0'; r++) {
355 if (line[r] == ':') {
356 if (line[w - 1] == ' ')
357 line[w - 1] = ':';
358 else
359 line[w++] = ':';
360 fieldname = 0;
361 while (line[r + 1] == ' ' ||
362 line[r + 1] == '\t')
363 r++;
364 continue;
366 if (line[r] == ' ' || line[r] == '\t') {
367 if (r != 0 && line[w - 1] == ' ')
368 continue;
369 else
370 line[w++] = ' ';
371 } else if (fieldname) {
372 line[w++] = tolower(line[r]);
373 continue;
374 } else
375 line[w++] = line[r];
377 linelen = line[w - 1] == ' ' ? w - 1 : w;
378 line[linelen] = '\0';
379 } else
380 linelen = strlen(line);
382 for (lastheader = 0; session->headers[lastheader] != NULL; lastheader++)
383 continue;
384 if (!session->lastheader) {
385 mtmp = reallocarray(session->headers, lastheader + 1,
386 sizeof(*mtmp));
387 if (mtmp == NULL) {
388 dkim_err(session, "Can't store header");
389 return;
391 session->headers = mtmp;
393 session->headers[lastheader] = strdup(line);
394 session->headers[lastheader + 1 ] = NULL;
395 session->lastheader = 1;
396 } else {
397 lastheader--;
398 linelen += strlen(session->headers[lastheader]);
399 if (canonheader == CANON_SIMPLE)
400 linelen += 2;
401 linelen++;
402 htmp = reallocarray(session->headers[lastheader], linelen,
403 sizeof(*htmp));
404 if (htmp == NULL) {
405 dkim_err(session, "Can't store header");
406 return;
408 session->headers[lastheader] = htmp;
409 if (canonheader == CANON_SIMPLE) {
410 if (strlcat(htmp, "\r\n", linelen) >= linelen)
411 fatalx("Missized header");
413 if (strlcat(htmp, line, linelen) >= linelen)
414 fatalx("Missized header");
418 void
419 dkim_parse_body(struct dkim_session *session, char *line)
421 size_t r, w;
422 size_t linelen;
423 if (line[0] == '\0') {
424 session->body_whitelines++;
425 return;
428 while (session->body_whitelines--) {
429 if (dkim_hash_update(session, "\r\n", 2) == 0)
430 return;
432 session->body_whitelines = 0;
434 session->has_body = 1;
435 if (canonbody == CANON_RELAXED) {
436 for (r = w = 0; line[r] != '\0'; r++) {
437 if (line[r] == ' ' || line[r] == '\t') {
438 if (r != 0 && line[w - 1] == ' ')
439 continue;
440 else
441 line[w++] = ' ';
442 } else
443 line[w++] = line[r];
445 linelen = line[w - 1] == ' ' ? w - 1 : w;
446 line[linelen] = '\0';
447 } else
448 linelen = strlen(line);
450 if (dkim_hash_update(session, line, linelen) == 0)
451 return;
452 if (dkim_hash_update(session, "\r\n", 2) == 0)
453 return;
456 void
457 dkim_built_signature(struct dkim_session *session)
459 char *signature;
460 size_t signaturesize = 1024;
461 size_t signaturelen = 0;
462 size_t linelen = 0;
463 size_t ncopied;
465 if ((signature = malloc(signaturesize)) == NULL) {
466 dkim_err(session, "Can't create signature");
467 return;
470 do {
471 ncopied = strlcpy(signature, "DKIM-Signature: v=1; a=",
472 signaturesize);
473 if (ncopied >= signaturesize)
474 if (cryptalg == CRYPT_RSA)
475 (void) strlcat(signature, "rsa", signaturesize);
476 if (hashalg == HASH_SHA1)
477 (void) strlcat(signature, "-sha1; ", signaturesize);
478 else
479 (void) strlcat(signature, "-sha256; ", signaturesize);
480 if (canonheader != CANON_SIMPLE || canonbody != CANON_SIMPLE) {
481 if (canonheader == CANON_SIMPLE)
482 (void) strlcat(signature, "c=simple", signaturesize);
483 else
484 (void) strlcat(signature, "c=relaxed", signaturesize);
486 if (canonbody != CANON_SIMPLE)
487 (void) strlcat(signature, "/relaxed", signaturesize);
489 (void) strlcat(signature, "d=", signaturesize);
490 (void) strlcat(signature, domain, signaturesize);
491 (void) strlcat(signature, "; ", signaturesize);
492 printf("%s\n", signature);
495 int
496 dkim_hash_update(struct dkim_session *session, char *buf, size_t len)
498 if (hashalg == HASH_SHA1) {
499 if (SHA1_Update(&(session->sha1), buf, len) == 0) {
500 dkim_errx(session, "Unable to update hash");
501 return 0;
503 } else {
504 if (SHA256_Update(&(session->sha256), buf, len) == 0) {
505 dkim_errx(session, "Unable to update hash");
506 return 0;
509 return 1;
512 int
513 dkim_hash_final(struct dkim_session *session, char *dest)
515 if (hashalg == HASH_SHA1) {
516 if (SHA1_Final(dest, &(session->sha1)) == 0) {
517 dkim_errx(session, "Unable to finalize hash");
518 return 0;
520 } else {
521 if (SHA256_Final(dest, &(session->sha256)) == 0) {
522 dkim_errx(session, "Unable to finalize hash");
523 return 0;
526 return 1;
529 __dead void
530 usage(void)
532 fprintf(stderr, "usage: %s [-a signalg] [-c canonicalization] -d domain -h headerfields\n", getprogname());
533 exit(1);
536 RB_GENERATE(dkim_sessions, dkim_session, entry, dkim_session_cmp);