]> code.citadel.org Git - citadel.git/blob - citadel/server/modules/smtp/dkim.c
f2a5d9d25aa8cdd0d2d9c6b4bec4d2751c90e0cb
[citadel.git] / citadel / server / modules / smtp / dkim.c
1 // DKIM signature creation
2 // https://www.rfc-editor.org/rfc/rfc6376.html
3 //
4 // Body canonicalization code (C) 2012 by Timothy E. Johansson
5 // The rest is written for Citadel and OpenSSL 3.0 (C) 2024 by Art Cancro
6 //
7 // This program is open source software.  Use, duplication, or disclosure is subject to the GNU General Public License v3.
8
9 // Make sure we don't accidentally use any deprecated API calls
10 #define OPENSSL_NO_DEPRECATED_3_0
11
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <stdio.h>
15 #include <ctype.h>
16 #include <string.h>
17 #include <time.h>
18 #include <assert.h>
19 #include <syslog.h>
20 #include <openssl/rand.h>
21 #include <openssl/rsa.h>
22 #include <openssl/engine.h>
23 #include <openssl/sha.h>
24 #include <openssl/hmac.h>
25 #include <openssl/evp.h>
26 #include <openssl/bio.h>
27 #include <openssl/pem.h>
28 #include <openssl/buffer.h>
29 #include <openssl/err.h>
30 #include <openssl/evp.h>
31 #include <libcitadel.h>
32
33 // This utility function is used by the body canonicalizer
34 char *dkim_rtrim(char *str) {
35         char *end;
36         int len = strlen(str);
37
38         while (*str && len) {
39                 end = str + len-1;
40                 
41                 if (*end == ' ' || *end == '\t') {
42                         *end = '\0';
43                 }
44                 else {
45                         break;
46                 }
47                 
48                 len = strlen(str);
49         }
50         
51         return str;
52 }
53
54
55 // We can use this handy function to wrap our big dkim signature header to a reasonable width
56 void dkim_wrap_header_strbuf(StrBuf *header_in) {
57         char *str = (char *)ChrPtr(header_in);
58         int len = StrLength(header_in);
59
60         char *tmp = malloc(len*3+1);
61         if (!tmp) {
62                 return;
63         }
64         int tmp_len = 0;
65         int i;
66         int lcount = 0;
67         
68         for (i = 0; i < len; ++i) {
69                 if (str[i] == ' ' || lcount == 75) {
70                         tmp[tmp_len++] = str[i];
71                         tmp[tmp_len++] = '\r';
72                         tmp[tmp_len++] = '\n';
73                         tmp[tmp_len++] = '\t';
74                         lcount = 0;
75                 }
76                 else {
77                         tmp[tmp_len++] = str[i];
78                         ++lcount;
79                 }
80         }
81         
82         tmp[tmp_len] = '\0';
83         StrBufPlain(header_in, tmp, tmp_len);
84         free(tmp);
85 }
86
87
88 // This utility function is used by the body canonicalizer
89 char *dkim_rtrim_lines(char *str) {
90         char *end;
91         int len = strlen(str);
92
93         while (*str && len) {
94                 end = str + len-1;
95                 
96                 if (*end == '\r' || *end == '\n') {
97                         *end = '\0';
98                 }
99                 else {
100                         break;
101                 }
102                 
103                 len = strlen(str);
104         }
105         
106         return str;
107 }
108
109
110 // Canonicalize one line of the message body as per the "relaxed" algorithm
111 char *dkim_canonicalize_body_line(char *line) {
112         int line_len = 0;
113         int i;
114         
115         // Ignores all whitespace at the end of lines.  Implementations MUST NOT remove the CRLF at the end of the line.
116         dkim_rtrim(line);
117         
118         // Reduces all sequences of whitespace within a line to a single space character.
119         line_len = strlen(line);
120         int new_len = 0;
121
122         for (i = 0; i < line_len; ++i) {
123                 if (line[i] == '\t') {
124                         line[i] = ' ';
125                 }
126         
127                 if (i > 0) {
128                         if (!(line[i-1] == ' ' && line[i] == ' ')) {
129                                 line[new_len++] = line[i];
130                         }
131                 }
132                 else {
133                         line[new_len++] = line[i];
134                 }
135         }
136         
137         line[new_len] = '\0';
138         return line;
139 }
140
141
142 // Canonicalize the message body as per the "relaxed" algorithm
143 char *dkim_canonicalize_body(char *body) {
144         int i = 0;
145         int offset = 0;
146         int body_len = strlen(body);
147
148         char *new_body = malloc(body_len*2+3);
149         int new_body_len = 0;
150
151         for (i = 0; i < body_len; ++i) {
152                 int is_r = 0;
153
154                 if (body[i] == '\n') {
155                         if (i > 0) {
156                                 if (body[i-1] == '\r') {
157                                         i--;
158                                         is_r = 1;
159                                 }
160                         }
161
162                         char *line = malloc(i - offset + 1);    
163                         memcpy(line, body+offset, i-offset);
164                         line[i-offset] = '\0';
165
166                         dkim_canonicalize_body_line(line);
167
168                         int line_len = strlen(line);
169                         memcpy(new_body+new_body_len, line, line_len);
170                         memcpy(new_body+new_body_len+line_len, "\r\n", 2);
171                         new_body_len += line_len+2;
172
173                         if (is_r) {
174                                 i++;
175                         }       
176
177                         offset = i+1;
178                         free(line);
179                 }
180         }
181
182         if (offset < body_len) {
183                 char *line = malloc(i - offset + 1);    
184                 memcpy(line, body+offset, i-offset);
185                 line[i-offset] = '\0';
186
187                 dkim_canonicalize_body_line(line);
188
189                 int line_len = strlen(line);
190                 memcpy(new_body+new_body_len, line, line_len);
191                 memcpy(new_body+new_body_len+line_len, "\r\n", 2);
192                 new_body_len += line_len+2;
193
194                 free(line);
195         }
196
197         memcpy(new_body+new_body_len, "\0", 1);
198
199         // Ignores all empty lines at the end of the message body.  "Empty line" is defined in Section 3.4.3.
200         new_body = dkim_rtrim_lines(new_body);
201
202         // Note that a completely empty or missing body is canonicalized as a
203         // single "CRLF"; that is, the canonicalized length will be 2 octets.
204         new_body_len = strlen(new_body);
205         new_body[new_body_len++] = '\r';
206         new_body[new_body_len++] = '\n';
207         new_body[new_body_len] = '\0';
208
209         return new_body;        
210 }
211
212
213 // First step to canonicalize a block of headers as per the "relaxed" algorithm.
214 // Unfold all headers onto single lines.
215 void dkim_unfold_headers(StrBuf *unfolded_headers) {
216         char *headers_start = (char *)ChrPtr(unfolded_headers);
217         char *fold;
218
219         while (
220                 fold = strstr(headers_start, "\r\n "),                          // find the first holded header
221                 fold = (fold ? fold : strstr(headers_start, "\r\n\t")),         // it could be folded with tabs
222                 fold != NULL                                                    // keep going until there aren't any left
223         ) {
224
225                 // Replace CRLF<space> or CRLF<tab> with CRLF
226                 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 3, HKEY("\r\n"));
227
228                 // And when we've got them all, remove the CRLF as well.
229                 if (
230                         (strstr(headers_start, "\r\n ") != fold)
231                         && (strstr(headers_start, "\r\n\t") != fold)
232                         && (!strncmp(fold, HKEY("\r\n")))
233                 ) {
234                         StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 2, HKEY(""));
235                 }
236
237         }
238 }
239
240
241 // Second step to canonicalize a block of headers as per the "relaxed" algorithm.
242 // Headers MUST already be unfolded with dkim_unfold_headers()
243 void dkim_canonicalize_unfolded_headers(StrBuf *headers) {
244
245         char *cheaders = (char *)ChrPtr(headers);
246         char *ptr = cheaders;
247         while (*ptr) {
248
249                 // We are at the beginning of a line.  Find the colon separator between field name and value.
250                 char *start_of_this_line = ptr;
251                 char *colon = strstr(ptr, ":");
252
253                 // remove whitespace after the colon
254                 while ( (*(colon+1) == ' ') || (*(colon+2) == '\t') ) {
255                         StrBufReplaceToken(headers, (long)(colon+1-cheaders), 1, HKEY(""));
256                 }
257                 char *end_of_this_line = strstr(ptr, "\r\n");
258
259                 // replace all multiple whitespace runs with a single space
260                 int replaced_something;
261                 do {
262                         replaced_something = 0;
263                         char *double_space = strstr(ptr, "  ");                 // space-space?
264                         if (!double_space) {
265                                 double_space = strstr(ptr, " \t");              // space-tab?
266                         }
267                         if (!double_space) {
268                                 double_space = strstr(ptr, "\t ");              // tab-space?
269                         }
270                         if (!double_space) {
271                                 double_space = strstr(ptr, "\t\t");             // tab-tab?
272                         }
273                         if (double_space) {
274                                 StrBufReplaceToken(headers, (long)(double_space-cheaders), 2, HKEY(" "));
275                                 ++replaced_something;
276                         }
277                 } while (replaced_something);
278
279                 // remove whitespace at the end of the line
280                 do {
281                         replaced_something = 0;
282                         char *trailing_space = strstr(ptr, " \r\n");            // line ends in a space?
283                         if (!trailing_space) {                                  // no?
284                                 trailing_space = strstr(ptr, "\t\r\n");         // how about a tab?
285                         }
286                         if (trailing_space) {
287                                 StrBufReplaceToken(headers, (long)(trailing_space-cheaders), 3, HKEY("\r\n"));
288                                 ++replaced_something;
289                         }
290                 } while (replaced_something);
291
292                 // Convert header field names to all lower case
293                 for (char *c = start_of_this_line; c<colon; ++c) {
294                         cheaders[c-cheaders] = tolower(cheaders[c-cheaders]);
295                 }
296
297                 ptr = end_of_this_line + 2;                                     // Advance to the beginning of the next line
298         }
299 }
300
301
302 // Third step to canonicalize a block of headers as per the "relaxed" algorithm.
303 // Reduce the canonicalized header block to only the fields being signed
304 void dkim_reduce_canonicalized_headers(StrBuf *headers, char *header_list) {
305
306         char *cheaders = (char *)ChrPtr(headers);
307         char *ptr = cheaders;
308         while (*ptr) {
309
310                 // We are at the beginning of a line.  Find the colon separator between field name and value.
311                 char *start_of_this_line = ptr;
312                 char *colon = strstr(ptr, ":");
313                 char *end_of_this_line = strstr(ptr, "\r\n");
314
315                 char relevant_headers[1024];
316                 strncpy(relevant_headers, header_list, sizeof(relevant_headers));
317                 char *rest = relevant_headers;
318                 char *token = NULL;
319                 int keep_this_header = 0;
320
321                 while (token = strtok_r(rest, ":", &rest)) {
322                         if (!strncmp(start_of_this_line, token, strlen(token))) {
323                                 keep_this_header = 1;
324                         }
325                 }
326
327                 if (keep_this_header) {                                          // Advance to the beginning of the next line
328                         ptr = end_of_this_line + 2;
329                 }
330                 else {
331                         StrBufReplaceToken(headers, (long)(start_of_this_line - cheaders), end_of_this_line-start_of_this_line+2, HKEY(""));
332                 }
333         }
334
335 }
336
337
338 // Make a new header list containing only the headers actually present in the canonicalized header block.
339 void dkim_final_header_list(char *header_list, size_t header_list_size, StrBuf *unfolded_headers) {
340         header_list[0] = 0;
341
342         char *cheaders = (char *)ChrPtr(unfolded_headers);
343         char *ptr = cheaders;
344         while (*ptr) {
345
346                 // We are at the beginning of a line.  Find the colon separator between field name and value.
347                 char *start_of_this_line = ptr;
348                 char *colon = strstr(ptr, ":");
349                 char *end_of_this_line = strstr(ptr, "\r\n");
350
351                 if (ptr != cheaders) {
352                         strcat(header_list, ":");
353                 }
354
355                 strncat(header_list, start_of_this_line, (colon-start_of_this_line));
356
357                 ptr = end_of_this_line + 2;                                     // Advance to the beginning of the next line
358         }
359 }
360
361
362 // Supplied with a PEM-encoded PKCS#7 private key, that might also have newlines replaced with underscores, return an EVP_PKEY.
363 // Caller is responsible for freeing it.
364 EVP_PKEY *dkim_import_key(char *pkey_in) {
365
366         if (!pkey_in) {
367                 return(NULL);
368         }
369
370         // Citadel Server stores the private key in PEM-encoded PKCS#7 format, but with all newlines replaced by underscores.
371         // Fix that before we try to decode it.
372         char *pkey_with_newlines = strdup(pkey_in);
373         if (!pkey_with_newlines) {
374                 return(NULL);
375         }
376         char *sp;
377         while (sp = strchr(pkey_with_newlines, '_')) {
378                 *sp = '\n';
379         }
380
381         // Load the private key into an OpenSSL "BIO" structure
382         BIO *bufio = BIO_new_mem_buf((void*)pkey_with_newlines, strlen(pkey_with_newlines));
383         if (bufio == NULL) {
384                 syslog(LOG_ERR, "dkim: BIO_new_mem_buf() failed");
385                 free(pkey_with_newlines);
386                 return(NULL);
387         }
388
389         // Now import the private key
390         EVP_PKEY *pkey = NULL;                  // Don't combine this line with the next one.  It will barf.
391         pkey = PEM_read_bio_PrivateKey(
392                 bufio,                          // BIO to read the private key from
393                 &pkey,                          // pointer to EVP_PKEY structure
394                 NULL,                           // password callback - can be NULL
395                 NULL                            // parameter passed to callback or password if callback is NULL
396         );
397
398         free(pkey_with_newlines);
399         BIO_free(bufio);
400
401         if (pkey == NULL) {
402                 syslog(LOG_ERR, "dkim: PEM_read_bio_PrivateKey() failed");
403         }
404
405         return(pkey);
406 }
407
408
409 // DKIM-sign an email, supplied as a full RFC2822-compliant message stored in a StrBuf
410 void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
411         int i = 0;
412
413         if (!email) {                                                           // no message was supplied
414                 return;
415         }
416
417         // Import the private key
418         EVP_PKEY *pkey = dkim_import_key(pkey_in);
419         if (pkey == NULL) {
420                 return;
421         }
422
423         // find the break between headers and body
424         size_t msglen = StrLength(email);                                       // total length of message (headers + body)
425
426         char *body_ptr = strstr(ChrPtr(email), "\r\n\r\n");
427         if (body_ptr == NULL) {
428                 syslog(LOG_ERR, "dkim: this message cannot be signed because it has no body");
429                 return;
430         }
431
432         size_t body_offset = body_ptr - ChrPtr(email);                          // offset at which message body begins
433         StrBuf *header_block = NewStrBufPlain(ChrPtr(email), body_offset+2);    // headers only (the +2 makes it include final CRLF)
434
435         // This removes the headers from the supplied email buffer.  We MUST put them back in later.
436         StrBufCutLeft(email, body_offset+4);                                    // The +4 makes it NOT include the CRLFCRLF
437
438         // Apply the "relaxed" canonicalization to the message body
439         char *relaxed_body = dkim_canonicalize_body((char *)ChrPtr(email));
440         int relaxed_body_len = strlen(relaxed_body);
441
442         // hash of the canonicalized body
443         unsigned char *body_hash = malloc(SHA256_DIGEST_LENGTH);
444         SHA256((unsigned char *)relaxed_body, relaxed_body_len, body_hash);
445         free(relaxed_body);                                                     // all we need now is the hash
446         relaxed_body = NULL;
447
448         // base64 encode the body hash
449         char *encoded_body_hash = malloc(SHA256_DIGEST_LENGTH * 2);
450         CtdlEncodeBase64(encoded_body_hash, body_hash, SHA256_DIGEST_LENGTH, BASE64_NO_LINEBREAKS);
451         free(body_hash);                                                        // all we need now is the encoded hash
452
453         // "relaxed" header canonicalization, step 1 : unfold the headers
454         StrBuf *unfolded_headers = NewStrBufDup(header_block);
455         dkim_unfold_headers(unfolded_headers);
456
457         // "relaxed" header canonicalization, step 2 : lowercase the header names, remove whitespace after the colon
458         dkim_canonicalize_unfolded_headers(unfolded_headers);
459
460         // "relaxed" header canonicalization, step 3 : reduce the canonicalized header block to only the fields being signed
461         char *header_list = "from:to:cc:reply-to:subject:date:list-unsubscribe:list-unsubscribe-post";
462         dkim_reduce_canonicalized_headers(unfolded_headers, header_list);
463
464         // Make a new header list containing only the ones we actually have.
465         char final_header_list[1024];
466         dkim_final_header_list(final_header_list, sizeof(final_header_list), unfolded_headers);
467
468         // create DKIM header
469         StrBuf *dkim_header = NewStrBuf();
470         StrBufPrintf(dkim_header,
471                 "v=1; a=rsa-sha256; s=%s; d=%s; l=%d; t=%d; c=relaxed/relaxed; h=%s; bh=%s; b=",
472                 selector,
473                 domain,
474                 relaxed_body_len,
475                 time(NULL),
476                 final_header_list,
477                 encoded_body_hash
478         );
479         free(encoded_body_hash);                                                // Hash is stored in the header now.
480
481         // Add the initial DKIM header (which is still missing the value after "b=") to the headers to be signed.
482         // RFC6376 3.7 tells us NOT to include CRLF after "b="
483         StrBufAppendBufPlain(unfolded_headers, HKEY("dkim-signature:"), 0);
484         StrBufAppendBuf(unfolded_headers, dkim_header, 0);
485
486         // Compute a hash of the canonicalized headers, and then sign that hash with our private key.
487         // RFC6376 says that we hash and sign everything up to the "b=" and then we'll add the rest at the end.
488
489         // The hashing/signing library calls are documented at https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying
490         // NOTE: EVP_DigestSign*() functions are supplied with the actual data to be hashed and signed.
491         // That means we don't hash it first, otherwise we would be signing double-hashed (and therefore wrong) data.
492
493         // Create the Message Digest Context
494         EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
495         if (mdctx == NULL) {
496                 syslog(LOG_ERR, "dkim: EVP_MD_CTX_create() failed with error 0x%lx", ERR_get_error());
497                 abort();
498         }
499
500         // Initialize the DigestSign operation using SHA-256 algorithm
501         if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
502                 syslog(LOG_ERR, "dkim: EVP_DigestSignInit() failed with error 0x%lx", ERR_get_error());
503                 abort();
504         }
505
506         // Call update with the "message" (the canonicalized headers)
507         if (EVP_DigestSignUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) != 1) {
508                 syslog(LOG_ERR, "dkim: EVP_DigestSignUpdate() failed with error 0x%lx", ERR_get_error());
509                 abort();
510         }
511
512         // Finalize the DigestSign operation.
513         // First call EVP_DigestSignFinal with a NULL sig parameter to obtain the length of the signature.
514         // Length is returned in signature_len
515         size_t signature_len;
516         if (EVP_DigestSignFinal(mdctx, NULL, &signature_len) != 1) {
517                 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
518                 abort();
519         }
520
521         // Sanity check.  The signature length should be the same as the size of the private key.
522         assert(signature_len == EVP_PKEY_size(pkey));
523
524         // Allocate memory for the signature based on size in signature_len
525         unsigned char *sig = OPENSSL_malloc(signature_len);
526         if (sig == NULL) {
527                 syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
528                 abort();
529         }
530
531         // Obtain the signature
532         if (EVP_DigestSignFinal(mdctx, sig, &signature_len) != 1) {
533                 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
534                 abort();
535         }
536         EVP_MD_CTX_free(mdctx);
537
538         // This is an optional routine to verify our own signature.
539         // The test program in tests/dkimtester enables it.  It is not enabled in Citadel Server.
540 #ifdef DKIM_VERIFY_SIGNATURE
541         mdctx = EVP_MD_CTX_new();
542         if (mdctx) {
543                 assert(EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1);
544                 assert(EVP_DigestVerifyUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) == 1);
545                 assert(EVP_DigestVerifyFinal(mdctx, sig, signature_len) == 1);
546                 EVP_MD_CTX_free(mdctx);
547         }
548 #endif
549
550         // With the signature complete, we no longer need the private key or the unfolded headers.
551         EVP_PKEY_free(pkey);
552         FreeStrBuf(&unfolded_headers);
553
554         // base64 encode the signature
555         char *encoded_signature = malloc(signature_len * 2);
556         int encoded_signature_len = CtdlEncodeBase64(encoded_signature, sig, signature_len, BASE64_NO_LINEBREAKS);
557         free(sig);                                                      // Free the raw signature, keep the b64-encoded one.
558         StrBufAppendPrintf(dkim_header, "%s", encoded_signature);       // Make the final header.
559         free(encoded_signature);
560
561         // wrap dkim header to 72-ish columns
562         dkim_wrap_header_strbuf(dkim_header);
563
564         // Now reassemble the message.
565         StrBuf *output_msg = NewStrBuf();
566         StrBufPrintf(output_msg, "DKIM-Signature: %s\r\n", (char *)ChrPtr(dkim_header));
567         StrBufAppendBuf(output_msg, header_block, 0);
568         StrBufAppendBufPlain(output_msg, HKEY("\r\n"), 0);
569         StrBufAppendBuf(output_msg, email, 0);
570
571         // Put the combined message where the caller can find it.
572         FreeStrBuf(&dkim_header);
573         FreeStrBuf(&header_block);
574         SwapBuffers(output_msg, email);
575         FreeStrBuf(&output_msg);
576
577         // And we're done!
578 }
579
580