]> code.citadel.org Git - citadel.git/blob - citadel/server/modules/smtp/dkim.c
Remove preprocessor tests for OpenSSL. It's a requirement.
[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
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 char *dkim_rtrim_lines(char *str) {
89         char *end;
90         int len = strlen(str);
91
92         while (*str && len) {
93                 end = str + len-1;
94                 
95                 if (*end == '\r' || *end == '\n') {
96                         *end = '\0';
97                 }
98                 else {
99                         break;
100                 }
101                 
102                 len = strlen(str);
103         }
104         
105         return str;
106 }
107
108
109 // Canonicalize one line of the message body as per the "relaxed" algorithm
110 char *dkim_canonicalize_body_line(char *line) {
111         int line_len = 0;
112         int i;
113         
114         // Ignores all whitespace at the end of lines.  Implementations MUST NOT remove the CRLF at the end of the line.
115         dkim_rtrim(line);
116         
117         // Reduces all sequences of whitespace within a line to a single space character.
118         line_len = strlen(line);
119         int new_len = 0;
120
121         for (i = 0; i < line_len; ++i) {
122                 if (line[i] == '\t') {
123                         line[i] = ' ';
124                 }
125         
126                 if (i > 0) {
127                         if (!(line[i-1] == ' ' && line[i] == ' ')) {
128                                 line[new_len++] = line[i];
129                         }
130                 }
131                 else {
132                         line[new_len++] = line[i];
133                 }
134         }
135         
136         line[new_len] = '\0';
137         return line;
138 }
139
140
141 // Canonicalize the message body as per the "relaxed" algorithm
142 char *dkim_canonicalize_body(char *body) {
143         int i = 0;
144         int offset = 0;
145         int body_len = strlen(body);
146
147         char *new_body = malloc(body_len*2+3);
148         int new_body_len = 0;
149
150         for (i = 0; i < body_len; ++i) {
151                 int is_r = 0;
152
153                 if (body[i] == '\n') {
154                         if (i > 0) {
155                                 if (body[i-1] == '\r') {
156                                         i--;
157                                         is_r = 1;
158                                 }
159                         }
160
161                         char *line = malloc(i - offset + 1);    
162                         memcpy(line, body+offset, i-offset);
163                         line[i-offset] = '\0';
164
165                         dkim_canonicalize_body_line(line);
166
167                         int line_len = strlen(line);
168                         memcpy(new_body+new_body_len, line, line_len);
169                         memcpy(new_body+new_body_len+line_len, "\r\n", 2);
170                         new_body_len += line_len+2;
171
172                         if (is_r) {
173                                 i++;
174                         }       
175
176                         offset = i+1;
177                         free(line);
178                 }
179         }
180
181         if (offset < body_len) {
182                 char *line = malloc(i - offset + 1);    
183                 memcpy(line, body+offset, i-offset);
184                 line[i-offset] = '\0';
185
186                 dkim_canonicalize_body_line(line);
187
188                 int line_len = strlen(line);
189                 memcpy(new_body+new_body_len, line, line_len);
190                 memcpy(new_body+new_body_len+line_len, "\r\n", 2);
191                 new_body_len += line_len+2;
192
193                 free(line);
194         }
195
196         memcpy(new_body+new_body_len, "\0", 1);
197
198         // Ignores all empty lines at the end of the message body.  "Empty line" is defined in Section 3.4.3.
199         new_body = dkim_rtrim_lines(new_body);
200
201         // Note that a completely empty or missing body is canonicalized as a
202         // single "CRLF"; that is, the canonicalized length will be 2 octets.
203         new_body_len = strlen(new_body);
204         new_body[new_body_len++] = '\r';
205         new_body[new_body_len++] = '\n';
206         new_body[new_body_len] = '\0';
207
208         return new_body;        
209 }
210
211
212 // First step to canonicalize a block of headers as per the "relaxed" algorithm.
213 // Unfold all headers onto single lines.
214 void dkim_unfold_headers(StrBuf *unfolded_headers) {
215         char *headers_start = (char *)ChrPtr(unfolded_headers);
216         char *fold;
217
218         while (
219                 fold = strstr(headers_start, "\r\n "),                          // find the first holded header
220                 fold = (fold ? fold : strstr(headers_start, "\r\n\t")),         // it could be folded with tabs
221                 fold != NULL                                                    // keep going until there aren't any left
222         ) {
223
224                 // Replace CRLF<space> or CRLF<tab> with CRLF
225                 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 3, HKEY("\r\n"));
226
227                 // And when we've got them all, remove the CRLF as well.
228                 if (
229                         (strstr(headers_start, "\r\n ") != fold)
230                         && (strstr(headers_start, "\r\n\t") != fold)
231                         && (!strncmp(fold, HKEY("\r\n")))
232                 ) {
233                         StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 2, HKEY(""));
234                 }
235
236         }
237 }
238
239
240 // Second step to canonicalize a block of headers as per the "relaxed" algorithm.
241 // Headers MUST already be unfolded with dkim_unfold_headers()
242 void dkim_canonicalize_unfolded_headers(StrBuf *headers) {
243
244         char *cheaders = (char *)ChrPtr(headers);
245         char *ptr = cheaders;
246         while (*ptr) {
247
248                 // We are at the beginning of a line.  Find the colon separator between field name and value.
249                 char *start_of_this_line = ptr;
250                 char *colon = strstr(ptr, ":");
251
252                 // remove whitespace after the colon
253                 while ( (*(colon+1) == ' ') || (*(colon+2) == '\t') ) {
254                         StrBufReplaceToken(headers, (long)(colon+1-cheaders), 1, HKEY(""));
255                 }
256                 char *end_of_this_line = strstr(ptr, "\r\n");
257
258                 // Convert header field names to all lower case
259                 for (char *c = start_of_this_line; c<colon; ++c) {
260                         cheaders[c-cheaders] = tolower(cheaders[c-cheaders]);
261                 }
262
263                 ptr = end_of_this_line + 2;                                     // Advance to the beginning of the next line
264         }
265 }
266
267
268 // Third step to canonicalize a block of headers as per the "relaxed" algorithm.
269 // Reduce the canonicalized header block to only the fields being signed
270 void dkim_reduce_canonicalized_headers(StrBuf *headers, char *header_list) {
271
272         char *cheaders = (char *)ChrPtr(headers);
273         char *ptr = cheaders;
274         while (*ptr) {
275
276                 // We are at the beginning of a line.  Find the colon separator between field name and value.
277                 char *start_of_this_line = ptr;
278                 char *colon = strstr(ptr, ":");
279                 char *end_of_this_line = strstr(ptr, "\r\n");
280
281                 char relevant_headers[1024];
282                 strncpy(relevant_headers, header_list, sizeof(relevant_headers));
283                 char *rest = relevant_headers;
284                 char *token = NULL;
285                 int keep_this_header = 0;
286
287                 while (token = strtok_r(rest, ":", &rest)) {
288                         if (!strncmp(start_of_this_line, token, strlen(token))) {
289                                 keep_this_header = 1;
290                         }
291                 }
292
293                 if (keep_this_header) {                                          // Advance to the beginning of the next line
294                         ptr = end_of_this_line + 2;
295                 }
296                 else {
297                         StrBufReplaceToken(headers, (long)(start_of_this_line - cheaders), end_of_this_line-start_of_this_line+2, HKEY(""));
298                 }
299         }
300
301 }
302
303
304 // Make a new header list containing only the headers actually present in the canonicalized header block.
305 void dkim_final_header_list(char *header_list, size_t header_list_size, StrBuf *unfolded_headers) {
306         header_list[0] = 0;
307
308         char *cheaders = (char *)ChrPtr(unfolded_headers);
309         char *ptr = cheaders;
310         while (*ptr) {
311
312                 // We are at the beginning of a line.  Find the colon separator between field name and value.
313                 char *start_of_this_line = ptr;
314                 char *colon = strstr(ptr, ":");
315                 char *end_of_this_line = strstr(ptr, "\r\n");
316
317                 if (ptr != cheaders) {
318                         strcat(header_list, ":");
319                 }
320
321                 strncat(header_list, start_of_this_line, (colon-start_of_this_line));
322
323                 ptr = end_of_this_line + 2;                                     // Advance to the beginning of the next line
324         }
325 }
326
327
328 // DKIM-sign an email, supplied as a full RFC2822-compliant message stored in a StrBuf
329 void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
330         int i = 0;
331
332         if (!email) {                                                           // no message was supplied
333                 return;
334         }
335
336         // find the break between headers and body
337         size_t msglen = StrLength(email);                                       // total length of message (headers + body)
338
339         char *body_ptr = strstr(ChrPtr(email), "\r\n\r\n");
340         if (body_ptr == NULL) {
341                 syslog(LOG_ERR, "dkim: this message cannot be signed because it has no body");
342                 return;
343         }
344
345         size_t body_offset = body_ptr - ChrPtr(email);                          // offset at which message body begins
346         StrBuf *header_block = NewStrBufPlain(ChrPtr(email), body_offset+2);    // headers only (the +2 makes it include final CRLF)
347
348         // This removes the headers from the supplied email buffer.  We MUST put them back in later.
349         StrBufCutLeft(email, body_offset+4);                                    // The +4 makes it NOT include the CRLFCRLF
350
351         // Apply the "relaxed" canonicalization to the message body
352         char *relaxed_body = dkim_canonicalize_body((char *)ChrPtr(email));
353         int relaxed_body_len = strlen(relaxed_body);
354
355         // hash of the canonicalized body
356         unsigned char *body_hash = malloc(SHA256_DIGEST_LENGTH);
357         SHA256((unsigned char*)relaxed_body, relaxed_body_len, body_hash);
358         free(relaxed_body);                                                     // all we need now is the hash
359         relaxed_body = NULL;
360
361         // base64 encode the body hash
362         char *encoded_body_hash = malloc(SHA256_DIGEST_LENGTH * 2);
363         CtdlEncodeBase64(encoded_body_hash, body_hash, SHA256_DIGEST_LENGTH, BASE64_NO_LINEBREAKS);
364         free(body_hash);                                                        // all we need now is the encoded hash
365
366         // "relaxed" header canonicalization, step 1 : unfold the headers
367         StrBuf *unfolded_headers = NewStrBufDup(header_block);
368         dkim_unfold_headers(unfolded_headers);
369
370         // "relaxed" header canonicalization, step 2 : lowercase the header names, remove whitespace after the colon
371         dkim_canonicalize_unfolded_headers(unfolded_headers);
372
373         // "relaxed" header canonicalization, step 3 : reduce the canonicalized header block to only the fields being signed
374         char *header_list = "from:to:cc:reply-to:subject:date:list-unsubscribe:list-unsubscribe-post";
375         dkim_reduce_canonicalized_headers(unfolded_headers, header_list);
376
377         // Make a new header list containing only the ones we actually have.
378         char final_header_list[1024];
379         dkim_final_header_list(final_header_list, sizeof(final_header_list), unfolded_headers);
380
381         // create DKIM header
382         StrBuf *dkim_header = NewStrBuf();
383         StrBufPrintf(dkim_header,
384                 "v=1; a=rsa-sha256; s=%s; d=%s; l=%d; t=%d; c=relaxed/relaxed; h=%s; bh=%s; b=",
385                 selector,
386                 domain,
387                 relaxed_body_len,
388                 time(NULL),
389                 final_header_list,
390                 encoded_body_hash
391         );
392         free(encoded_body_hash);                                                // Hash is stored in the header now.
393
394         // Add the initial DKIM header (which is still missing the value after "b=") to the headers to be signed.
395         // RFC6376 3.7 tells us NOT to include CRLF after "b="
396         StrBufAppendBufPlain(unfolded_headers, HKEY("dkim-signature:"), 0);
397         StrBufAppendBuf(unfolded_headers, dkim_header, 0);
398
399         // Compute a hash of the canonicalized headers, and then sign that hash with our private key.
400         // RFC6376 says that we hash and sign everything up to the "b=" and then we'll add the rest at the end.
401
402         // Load the private key into an OpenSSL "BIO" structure
403         BIO *bufio = BIO_new_mem_buf((void*)pkey_in, strlen(pkey_in));
404         if (bufio == NULL) {
405                 syslog(LOG_ERR, "dkim: BIO_new_mem_buf() failed");
406                 abort();
407         }
408
409         // Now import the private key
410         EVP_PKEY *pkey = NULL;                  // Don't combine this line with the next one.  It will barf.
411         pkey = PEM_read_bio_PrivateKey(
412                 bufio,                          // BIO to read the private key from
413                 &pkey,                          // pointer to EVP_PKEY structure
414                 NULL,                           // password callback - can be NULL
415                 NULL                            // parameter passed to callback or password if callback is NULL
416         );
417         if (pkey == NULL) {
418                 syslog(LOG_ERR, "dkim: PEM_read_bio_PrivateKey() failed");
419                 abort();
420         }
421         BIO_free(bufio);                        // Don't need this anymore, we have `pkey` now
422
423         // The hashing/signing library calls are documented at https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying
424         // NOTE: EVP_DigestSign*() functions are supplied with the actual data to be hashed and signed.
425         // That means we don't hash it first, otherwise we would be signing double-hashed (and therefore wrong) data.
426
427         // Create the Message Digest Context
428         EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
429         if (mdctx == NULL) {
430                 syslog(LOG_ERR, "dkim: EVP_MD_CTX_create() failed with error 0x%lx", ERR_get_error());
431                 abort();
432         }
433
434         // Initialize the DigestSign operation using SHA-256 algorithm
435         if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
436                 syslog(LOG_ERR, "dkim: EVP_DigestSignInit() failed with error 0x%lx", ERR_get_error());
437                 abort();
438         }
439
440         // Call update with the "message" (the canonicalized headers)
441         if (EVP_DigestSignUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) != 1) {
442                 syslog(LOG_ERR, "dkim: EVP_DigestSignUpdate() failed with error 0x%lx", ERR_get_error());
443                 abort();
444         }
445
446         // Finalize the DigestSign operation.
447         // First call EVP_DigestSignFinal with a NULL sig parameter to obtain the length of the signature.
448         // Length is returned in signature_len
449         size_t signature_len;
450         if (EVP_DigestSignFinal(mdctx, NULL, &signature_len) != 1) {
451                 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
452                 abort();
453         }
454
455         // Sanity check.  The signature length should be the same as the size of the private key.
456         assert(signature_len == EVP_PKEY_size(pkey));
457
458         // Allocate memory for the signature based on size in signature_len
459         unsigned char *sig = OPENSSL_malloc(signature_len);
460         if (sig == NULL) {
461                 syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
462                 abort();
463         }
464
465         // Obtain the signature
466         if (EVP_DigestSignFinal(mdctx, sig, &signature_len) != 1) {
467                 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
468                 abort();
469         }
470         EVP_MD_CTX_free(mdctx);
471
472         // This is an optional routine to verify our own signature.
473         // The test program in tests/dkimtester enables it.  It is not enabled during server operation.
474 #ifdef DKIM_VERIFY_SIGNATURE
475         mdctx = EVP_MD_CTX_new();
476         if (mdctx) {
477                 assert(EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1);
478                 assert(EVP_DigestVerifyUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) == 1);
479                 assert(EVP_DigestVerifyFinal(mdctx, sig, signature_len) == 1);
480                 EVP_MD_CTX_free(mdctx);
481         }
482 #endif
483
484         // With the signature complete, we no longer need the private key or the unfolded headers.
485         EVP_PKEY_free(pkey);
486         FreeStrBuf(&unfolded_headers);
487
488         // base64 encode the signature
489         char *encoded_signature = malloc(signature_len * 2);
490         int encoded_signature_len = CtdlEncodeBase64(encoded_signature, sig, signature_len, BASE64_NO_LINEBREAKS);
491         free(sig);                                                      // Free the raw signature, keep the b64-encoded one.
492         StrBufAppendPrintf(dkim_header, "%s", encoded_signature);       // Make the final header.
493         free(encoded_signature);
494
495         // wrap dkim header to 72-ish columns
496         dkim_wrap_header_strbuf(dkim_header);
497
498         // Now reassemble the message.
499         StrBuf *output_msg = NewStrBuf();
500         StrBufPrintf(output_msg, "DKIM-Signature: %s\r\n", (char *)ChrPtr(dkim_header));
501         StrBufAppendBuf(output_msg, header_block, 0);
502         StrBufAppendBufPlain(output_msg, HKEY("\r\n"), 0);
503         StrBufAppendBuf(output_msg, email, 0);
504
505         // Put the combined message where the caller can find it.
506         FreeStrBuf(&dkim_header);
507         FreeStrBuf(&header_block);
508         SwapBuffers(output_msg, email);
509         FreeStrBuf(&output_msg);
510
511         // And we're done!
512 }