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>
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 char *dkim_rtrim_lines(char *str) {
90 int len = strlen(str);
95 if (*end == '\r' || *end == '\n') {
109 // Canonicalize one line of the message body as per the "relaxed" algorithm
110 char *dkim_canonicalize_body_line(char *line) {
114 // Ignores all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line.
117 // Reduces all sequences of whitespace within a line to a single space character.
118 line_len = strlen(line);
121 for (i = 0; i < line_len; ++i) {
122 if (line[i] == '\t') {
127 if (!(line[i-1] == ' ' && line[i] == ' ')) {
128 line[new_len++] = line[i];
132 line[new_len++] = line[i];
136 line[new_len] = '\0';
141 // Canonicalize the message body as per the "relaxed" algorithm
142 char *dkim_canonicalize_body(char *body) {
145 int body_len = strlen(body);
147 char *new_body = malloc(body_len*2+3);
148 int new_body_len = 0;
150 for (i = 0; i < body_len; ++i) {
153 if (body[i] == '\n') {
155 if (body[i-1] == '\r') {
161 char *line = malloc(i - offset + 1);
162 memcpy(line, body+offset, i-offset);
163 line[i-offset] = '\0';
165 dkim_canonicalize_body_line(line);
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;
181 if (offset < body_len) {
182 char *line = malloc(i - offset + 1);
183 memcpy(line, body+offset, i-offset);
184 line[i-offset] = '\0';
186 dkim_canonicalize_body_line(line);
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;
196 memcpy(new_body+new_body_len, "\0", 1);
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);
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';
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);
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
224 // Replace CRLF<space> or CRLF<tab> with CRLF
225 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 3, HKEY("\r\n"));
227 // And when we've got them all, remove the CRLF as well.
229 (strstr(headers_start, "\r\n ") != fold)
230 && (strstr(headers_start, "\r\n\t") != fold)
231 && (!strncmp(fold, HKEY("\r\n")))
233 StrBufReplaceToken(unfolded_headers, (long)(fold-headers_start), 2, HKEY(""));
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) {
244 char *cheaders = (char *)ChrPtr(headers);
245 char *ptr = cheaders;
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, ":");
252 // remove whitespace after the colon
253 while ( (*(colon+1) == ' ') || (*(colon+2) == '\t') ) {
254 StrBufReplaceToken(headers, (long)(colon+1-cheaders), 1, HKEY(""));
256 char *end_of_this_line = strstr(ptr, "\r\n");
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]);
263 ptr = end_of_this_line + 2; // Advance to the beginning of the next line
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) {
272 char *cheaders = (char *)ChrPtr(headers);
273 char *ptr = cheaders;
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");
281 char relevant_headers[1024];
282 strncpy(relevant_headers, header_list, sizeof(relevant_headers));
283 char *rest = relevant_headers;
285 int keep_this_header = 0;
287 while (token = strtok_r(rest, ":", &rest)) {
288 if (!strncmp(start_of_this_line, token, strlen(token))) {
289 keep_this_header = 1;
293 if (keep_this_header) { // Advance to the beginning of the next line
294 ptr = end_of_this_line + 2;
297 StrBufReplaceToken(headers, (long)(start_of_this_line - cheaders), end_of_this_line-start_of_this_line+2, HKEY(""));
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) {
308 char *cheaders = (char *)ChrPtr(unfolded_headers);
309 char *ptr = cheaders;
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");
317 if (ptr != cheaders) {
318 strcat(header_list, ":");
321 strncat(header_list, start_of_this_line, (colon-start_of_this_line));
323 ptr = end_of_this_line + 2; // Advance to the beginning of the next line
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) {
332 if (!email) { // no message was supplied
336 // find the break between headers and body
337 size_t msglen = StrLength(email); // total length of message (headers + body)
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");
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)
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
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);
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
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
366 // "relaxed" header canonicalization, step 1 : unfold the headers
367 StrBuf *unfolded_headers = NewStrBufDup(header_block);
368 dkim_unfold_headers(unfolded_headers);
370 // "relaxed" header canonicalization, step 2 : lowercase the header names, remove whitespace after the colon
371 dkim_canonicalize_unfolded_headers(unfolded_headers);
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);
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);
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=",
392 free(encoded_body_hash); // Hash is stored in the header now.
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);
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.
402 // Load the private key into an OpenSSL "BIO" structure
403 BIO *bufio = BIO_new_mem_buf((void*)pkey_in, strlen(pkey_in));
405 syslog(LOG_ERR, "dkim: BIO_new_mem_buf() failed");
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
418 syslog(LOG_ERR, "dkim: PEM_read_bio_PrivateKey() failed");
421 BIO_free(bufio); // Don't need this anymore, we have `pkey` now
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.
427 // Create the Message Digest Context
428 EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
430 syslog(LOG_ERR, "dkim: EVP_MD_CTX_create() failed with error 0x%lx", ERR_get_error());
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());
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());
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");
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));
458 // Allocate memory for the signature based on size in signature_len
459 unsigned char *sig = OPENSSL_malloc(signature_len);
461 syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
465 // Obtain the signature
466 if (EVP_DigestSignFinal(mdctx, sig, &signature_len) != 1) {
467 syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
470 EVP_MD_CTX_free(mdctx);
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();
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);
484 // With the signature complete, we no longer need the private key or the unfolded headers.
486 FreeStrBuf(&unfolded_headers);
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);
495 // wrap dkim header to 72-ish columns
496 dkim_wrap_header_strbuf(dkim_header);
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);
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);