]> code.citadel.org Git - citadel.git/commitdiff
update dkim.c from other repository
authorArt Cancro <ajc@citadel.org>
Fri, 10 May 2024 19:02:16 +0000 (19:02 +0000)
committerArt Cancro <ajc@citadel.org>
Fri, 10 May 2024 19:02:16 +0000 (19:02 +0000)
citadel/server/modules/smtp/dkim.c

index 7549d501312374fecc1a79c0d84d262698074fcc..172c89f77a83f22f29ccafcbb5dc4a004f9b1483 100644 (file)
@@ -1,7 +1,9 @@
 // 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
@@ -13,8 +15,8 @@
 #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>
@@ -50,6 +52,39 @@ char *dkim_rtrim(char *str) {
 }
 
 
+// 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);
@@ -335,22 +370,19 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
 
        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);
@@ -362,9 +394,6 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
        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);
@@ -376,42 +405,31 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
        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) {
@@ -433,13 +451,9 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
        }
        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();
@@ -455,7 +469,6 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
        }
 
        // 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();
@@ -463,60 +476,64 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
 
        // 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);