// DKIM signature creation
-// https://www.rfc-editor.org/rfc/rfc6376.html#section-5
+// https://www.rfc-editor.org/rfc/rfc6376.html
+//
+// Trim code borrowed from firm-dkim (C) 2012 by Timothy E. Johansson
+// The rest is written for Citadel and OpenSSL 3.0 (C) 2024 by Art Cancro
//
-// Copyright (c) 2024 by Art Cancro
// This program is open source software. Use, duplication, or disclosure is subject to the GNU General Public License v3.
// Make sure we don't accidentally use any deprecated API calls
#include <ctype.h>
#include <string.h>
#include <time.h>
-#include <syslog.h>
#include <assert.h>
+#include <syslog.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
#include <openssl/engine.h>
}
+// We can use this handy function to wrap our big dkim signature header to a reasonable width
+void dkim_wrap_header_strbuf(StrBuf *header_in) {
+ char *str = (char *)ChrPtr(header_in);
+ int len = StrLength(header_in);
+
+ char *tmp = malloc(len*3+1);
+ if (!tmp) {
+ return;
+ }
+ int tmp_len = 0;
+ int i;
+ int lcount = 0;
+
+ for (i = 0; i < len; ++i) {
+ if (str[i] == ' ' || lcount == 75) {
+ tmp[tmp_len++] = str[i];
+ tmp[tmp_len++] = '\r';
+ tmp[tmp_len++] = '\n';
+ tmp[tmp_len++] = '\t';
+ lcount = 0;
+ }
+ else {
+ tmp[tmp_len++] = str[i];
+ ++lcount;
+ }
+ }
+
+ tmp[tmp_len] = '\0';
+ StrBufPlain(header_in, tmp, tmp_len);
+ free(tmp);
+}
+
+
char *dkim_rtrim_lines(char *str) {
char *end;
int len = strlen(str);
char *body_ptr = strstr(ChrPtr(email), "\r\n\r\n");
if (body_ptr == NULL) {
- syslog(LOG_DEBUG, "dkim: this message cannot be signed because it has no body");
+ syslog(LOG_ERR, "dkim: this message cannot be signed because it has no body");
return;
}
size_t body_offset = body_ptr - ChrPtr(email); // offset at which message body begins
StrBuf *header_block = NewStrBufPlain(ChrPtr(email), body_offset+2); // headers only (the +2 makes it include final CRLF)
- //syslog(LOG_DEBUG, "Supplied headers:\n-----\n%s-----\n", ChrPtr(header_block));
// This removes the headers from the supplied email buffer. We MUST put them back in later.
StrBufCutLeft(email, body_offset+4); // The +4 makes it NOT include the CRLFCRLF
- //syslog(LOG_DEBUG, "Body:\n-----\n%s-----\n", ChrPtr(email));
// Apply the "relaxed" canonicalization to the message body
char *relaxed_body = dkim_canonicalize_body((char *)ChrPtr(email));
int relaxed_body_len = strlen(relaxed_body);
- //syslog(LOG_DEBUG, "Canonicalized body:\n-----\n%s-----\n", relaxed_body);
// hash of the canonicalized body
unsigned char *body_hash = malloc(SHA256_DIGEST_LENGTH);
char *encoded_body_hash = dkim_base64_encode(body_hash, SHA256_DIGEST_LENGTH);
free(body_hash); // all we need now is the encoded hash
- // In the test harness email, the body hash should be 2PMBIMGyD2GZlndKI2MRsbhiMr6jD5rCxhq+mCvY7os=
- syslog(LOG_DEBUG, "dkim: body hash: %s", encoded_body_hash);
-
// "relaxed" header canonicalization, step 1 : unfold the headers
StrBuf *unfolded_headers = NewStrBufDup(header_block);
dkim_unfold_headers(unfolded_headers);
char *header_list = "from:to:cc:reply-to:subject:date:list-unsubscribe:list-unsubscribe-post";
dkim_reduce_canonicalized_headers(unfolded_headers, header_list);
- //syslog(LOG_DEBUG, "%s", (char *)ChrPtr(unfolded_headers));
- //syslog(LOG_DEBUG, "\033[34m-----(final_len=%d)\033[0m\n", StrLength(unfolded_headers));
-
// Make a new header list containing only the ones we actually have.
char final_header_list[1024];
dkim_final_header_list(final_header_list, sizeof(final_header_list), unfolded_headers);
- syslog(LOG_DEBUG, "dkim: final header list: %s", final_header_list);
// create DKIM header
- time_t now = time(NULL); // signature timestamp
StrBuf *dkim_header = NewStrBuf();
StrBufPrintf(dkim_header,
"v=1; a=rsa-sha256; s=%s; d=%s; l=%d; t=%d; c=relaxed/relaxed; h=%s; bh=%s; b=",
selector,
domain,
relaxed_body_len,
- now,
+ time(NULL),
final_header_list,
encoded_body_hash
);
- free(encoded_body_hash); // Don't need this anymore either.
+ free(encoded_body_hash); // Hash is stored in the header now.
// Add the initial DKIM header (which is still missing the value after "b=") to the headers to be signed.
// RFC6376 3.7 tells us NOT to include CRLF after "b="
StrBufAppendBufPlain(unfolded_headers, HKEY("dkim-signature:"), 0);
StrBufAppendBuf(unfolded_headers, dkim_header, 0);
- //syslog(LOG_DEBUG, "%s", (char *)ChrPtr(unfolded_headers));
- //syslog(LOG_DEBUG, "\n\033[34m-----(final_len=%d)\033[0m\n", StrLength(unfolded_headers));
// Compute a hash of the canonicalized headers, and then sign that hash with our private key.
// RFC6376 says that we hash and sign everything up to the "b=" and then we'll add the rest at the end.
- // Don't need to do this -- the EVP_DigestSign* functions do both the hashing and signature for us.
- //unsigned char *headers_hash = malloc(SHA256_DIGEST_LENGTH);
- //SHA256((unsigned char*)ChrPtr(unfolded_headers), StrLength(unfolded_headers), headers_hash);
-
// Load the private key into an OpenSSL "BIO" structure
BIO *bufio = BIO_new_mem_buf((void*)pkey_in, strlen(pkey_in));
if (bufio == NULL) {
}
BIO_free(bufio); // Don't need this anymore, we have `pkey` now
-
- // ok, here's where we are on thursday night, may 9.
- // It's correctly signing *something* , because when I change the private key, the test utility
- // recognizes that the signature doesn't match the public key. So we're likely either creating a bad
- // digest, or signing it wrong.
-
// The hashing/signing library calls are documented at https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying
+ // NOTE: EVP_DigestSign*() functions are supplied with the actual data to be hashed and signed.
+ // That means we don't hash it first, otherwise we would be signing double-hashed (and therefore wrong) data.
// Create the Message Digest Context
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
}
// Call update with the "message" (the canonicalized headers)
- // Hey ... I think we need to update with the actual data, not a pre-calculated hash, since it does its own hash
if (EVP_DigestSignUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) != 1) {
syslog(LOG_ERR, "dkim: EVP_DigestSignUpdate() failed with error 0x%lx", ERR_get_error());
abort();
// Finalize the DigestSign operation.
// First call EVP_DigestSignFinal with a NULL sig parameter to obtain the length of the signature.
- // Length is returned in slen
- size_t slen;
- if (EVP_DigestSignFinal(mdctx, NULL, &slen) != 1) {
+ // Length is returned in signature_len
+ size_t signature_len;
+ if (EVP_DigestSignFinal(mdctx, NULL, &signature_len) != 1) {
syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
abort();
}
// Sanity check. The signature length should be the same as the size of the private key.
- assert(slen == EVP_PKEY_size(pkey));
+ assert(signature_len == EVP_PKEY_size(pkey));
- // Allocate memory for the signature based on size in slen
- unsigned char *sig = OPENSSL_malloc(slen);
+ // Allocate memory for the signature based on size in signature_len
+ unsigned char *sig = OPENSSL_malloc(signature_len);
if (sig == NULL) {
syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
abort();
}
// Obtain the signature
- if (EVP_DigestSignFinal(mdctx, sig, &slen) != 1) {
+ if (EVP_DigestSignFinal(mdctx, sig, &signature_len) != 1) {
syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
abort();
}
+ EVP_MD_CTX_free(mdctx);
- // Verify the signature. We can comment this bit out when we're done.
- EVP_MD_CTX *md_ctx_verify = EVP_MD_CTX_new();
- if (md_ctx_verify) {
- EVP_DigestVerifyInit(md_ctx_verify, NULL, EVP_sha256(), NULL, pkey);
- EVP_DigestVerifyUpdate(md_ctx_verify, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers));
- syslog(LOG_DEBUG, "Verify signature: %s", (EVP_DigestVerifyFinal(md_ctx_verify, sig, slen)) ? "PASS" : "FAIL");
- EVP_MD_CTX_free(md_ctx_verify);
+ // THIS IS OPTIONAL. Do it a second time, but verify the signature instead of signing.
+ mdctx = EVP_MD_CTX_new();
+ if (mdctx) {
+ assert(EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1);
+ assert(EVP_DigestVerifyUpdate(mdctx, (char *)ChrPtr(unfolded_headers), StrLength(unfolded_headers)) == 1);
+ assert(EVP_DigestVerifyFinal(mdctx, sig, signature_len) == 1);
+ EVP_MD_CTX_free(mdctx);
}
// End verify
- // ok we're done with all this crap now
- EVP_MD_CTX_free(mdctx);
+ // With the signature complete, we no longer need the private key or the unfolded headers.
EVP_PKEY_free(pkey);
FreeStrBuf(&unfolded_headers);
// base64 encode the signature
- char *sig_b64 = dkim_base64_encode(sig, slen);
- int sig_b64_len = strlen(sig_b64);
+ char *encoded_signature = dkim_base64_encode(sig, signature_len);
+ int encoded_signature_len = strlen(encoded_signature);
free(sig); // Free the raw signature, keep the b64-encoded one.
-
- // wrap dkim header
- // sig_b64 = dkim_wrap(sig_b64, sig_b64_len);
+ StrBufAppendPrintf(dkim_header, "%s", encoded_signature); // Make the final header.
+ free(encoded_signature);
+
+ // wrap dkim header to 72-ish columns
+ dkim_wrap_header_strbuf(dkim_header);
+ // Now reassemble the message.
StrBuf *output_msg = NewStrBuf();
- StrBufPrintf(output_msg, "DKIM-Signature: %s%s\r\n", (char *)ChrPtr(dkim_header), sig_b64);
- FreeStrBuf(&dkim_header); // These were added to the final header block.
- free(sig_b64); // So we don't need them anymore.
+ StrBufPrintf(output_msg, "DKIM-Signature: %s\r\n", (char *)ChrPtr(dkim_header));
StrBufAppendBuf(output_msg, header_block, 0);
- FreeStrBuf(&header_block);
- StrBufAppendPrintf(output_msg, "\r\n");
+ StrBufAppendBufPlain(output_msg, HKEY("\r\n"), 0);
StrBufAppendBuf(output_msg, email, 0);
+
+ // Put the combined message where the caller can find it.
+ FreeStrBuf(&dkim_header);
+ FreeStrBuf(&header_block);
SwapBuffers(output_msg, email);
FreeStrBuf(&output_msg);