1 // DKIM signature creation
2 // https://www.rfc-editor.org/rfc/rfc6376.html
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
7 // This program is open source software. Use, duplication, or disclosure is subject to the GNU General Public License v3.
9 // Make sure we don't accidentally use any deprecated API calls
10 #define OPENSSL_NO_DEPRECATED_3_0
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>
33 // This utility function is used by the body canonicalizer
34 char *dkim_rtrim(char *str) {
36 int len = strlen(str);
41 if (*end == ' ' || *end == '\t') {
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);
60 char *tmp = malloc(len*3+1);
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';
77 tmp[tmp_len++] = str[i];
83 StrBufPlain(header_in, tmp, tmp_len);
88 // This utility function is used by the body canonicalizer
89 char *dkim_rtrim_lines(char *str) {
91 int len = strlen(str);
96 if (*end == '\r' || *end == '\n') {
110 // Canonicalize one line of the message body as per the "relaxed" algorithm
111 char *dkim_canonicalize_body_line(char *line) {
115 // Ignores all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line.
118 // Reduces all sequences of whitespace within a line to a single space character.
119 line_len = strlen(line);
122 for (i = 0; i < line_len; ++i) {
123 if (line[i] == '\t') {
128 if (!(line[i-1] == ' ' && line[i] == ' ')) {
129 line[new_len++] = line[i];
133 line[new_len++] = line[i];
137 line[new_len] = '\0';
142 // Canonicalize the message body as per the "relaxed" algorithm
143 char *dkim_canonicalize_body(char *body) {
146 int body_len = strlen(body);
148 char *new_body = malloc(body_len*2+3);
149 int new_body_len = 0;
151 for (i = 0; i < body_len; ++i) {
154 if (body[i] == '\n') {
156 if (body[i-1] == '\r') {
162 char *line = malloc(i - offset + 1);
163 memcpy(line, body+offset, i-offset);
164 line[i-offset] = '\0';
166 dkim_canonicalize_body_line(line);
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;
182 if (offset < body_len) {
183 char *line = malloc(i - offset + 1);
184 memcpy(line, body+offset, i-offset);
185 line[i-offset] = '\0';
187 dkim_canonicalize_body_line(line);
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;
197 memcpy(new_body+new_body_len, "\0", 1);
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);
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';
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);
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
225 // Replace CRLF<space> or CRLF<tab> with CRLF
226 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 3, HKEY("\r\n"));
228 // And when we've got them all, remove the CRLF as well.
230 (strstr(headers_start, "\r\n ") != fold)
231 && (strstr(headers_start, "\r\n\t") != fold)
232 && (!strncmp(fold, HKEY("\r\n")))
234 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 2, HKEY(""));
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) {
245 char *cheaders = (char *)ChrPtr(headers);
246 char *ptr = cheaders;
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, ":");
253 // remove whitespace after the colon
254 while ( (*(colon+1) == ' ') || (*(colon+2) == '\t') ) {
255 StrBufReplaceToken(headers, (long)(colon+1-cheaders), 1, HKEY(""));
257 char *end_of_this_line = strstr(ptr, "\r\n");
259 // replace all multiple whitespace runs with a single space
260 int replaced_something;
262 replaced_something = 0;
263 char *double_space = strstr(ptr, " "); // space-space?
265 double_space = strstr(ptr, " \t"); // space-tab?
268 double_space = strstr(ptr, "\t "); // tab-space?
271 double_space = strstr(ptr, "\t\t"); // tab-tab?
274 StrBufReplaceToken(headers, (long)(double_space-cheaders), 2, HKEY(" "));
275 ++replaced_something;
277 } while (replaced_something);
279 // remove whitespace at the end of the line
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?
286 if (trailing_space) {
287 StrBufReplaceToken(headers, (long)(trailing_space-cheaders), 3, HKEY("\r\n"));
288 ++replaced_something;
290 } while (replaced_something);
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]);
297 ptr = end_of_this_line + 2; // Advance to the beginning of the next line
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) {
306 char *cheaders = (char *)ChrPtr(headers);
307 char *ptr = cheaders;
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");
315 char relevant_headers[1024];
316 strncpy(relevant_headers, header_list, sizeof(relevant_headers));
317 char *rest = relevant_headers;
319 int keep_this_header = 0;
321 while (token = strtok_r(rest, ":", &rest)) {
322 if (!strncmp(start_of_this_line, token, strlen(token))) {
323 keep_this_header = 1;
327 if (keep_this_header) { // Advance to the beginning of the next line
328 ptr = end_of_this_line + 2;
331 StrBufReplaceToken(headers, (long)(start_of_this_line - cheaders), end_of_this_line-start_of_this_line+2, HKEY(""));
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) {
342 char *cheaders = (char *)ChrPtr(unfolded_headers);
343 char *ptr = cheaders;
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");
351 if (ptr != cheaders) {
352 strcat(header_list, ":");
355 strncat(header_list, start_of_this_line, (colon-start_of_this_line));
357 ptr = end_of_this_line + 2; // Advance to the beginning of the next line
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) {
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) {
377 while (sp = strchr(pkey_with_newlines, '_')) {
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));
384 syslog(LOG_ERR, "dkim: BIO_new_mem_buf() failed");
385 free(pkey_with_newlines);
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
398 free(pkey_with_newlines);
402 syslog(LOG_ERR, "dkim: PEM_read_bio_PrivateKey() failed");
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) {
413 if (!email) { // no message was supplied
417 // Import the private key
418 EVP_PKEY *pkey = dkim_import_key(pkey_in);
423 // find the break between headers and body
424 size_t msglen = StrLength(email); // total length of message (headers + body)
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");
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)
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
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);
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
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
453 // "relaxed" header canonicalization, step 1 : unfold the headers
454 StrBuf *unfolded_headers = NewStrBufDup(header_block);
455 dkim_unfold_headers(unfolded_headers);
457 // "relaxed" header canonicalization, step 2 : lowercase the header names, remove whitespace after the colon
458 dkim_canonicalize_unfolded_headers(unfolded_headers);
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);
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);
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=",
479 free(encoded_body_hash); // Hash is stored in the header now.
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);
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.
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.
493 // Create the Message Digest Context
494 EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
496 syslog(LOG_ERR, "dkim: EVP_MD_CTX_create() failed with error 0x%lx", ERR_get_error());
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());
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());
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");
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));
524 // Allocate memory for the signature based on size in signature_len
525 unsigned char *sig = OPENSSL_malloc(signature_len);
527 syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
531 // Obtain the signature
532 if (EVP_DigestSignFinal(mdctx, sig, &signature_len) != 1) {
533 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
536 EVP_MD_CTX_free(mdctx);
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();
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);
550 // With the signature complete, we no longer need the private key or the unfolded headers.
552 FreeStrBuf(&unfolded_headers);
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);
561 // wrap dkim header to 72-ish columns
562 dkim_wrap_header_strbuf(dkim_header);
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);
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);