]> code.citadel.org Git - citadel.git/commitdiff
Bring in new dkim code
authorArt Cancro <ajc@citadel.org>
Fri, 10 May 2024 04:50:26 +0000 (04:50 +0000)
committerArt Cancro <ajc@citadel.org>
Fri, 10 May 2024 04:50:26 +0000 (04:50 +0000)
citadel/server/modules/smtp/dkim.c

index 97af4b6bd4a1b09f82a5d6b80b3a546fb5c88405..7549d501312374fecc1a79c0d84d262698074fcc 100644 (file)
@@ -13,6 +13,8 @@
 #include <ctype.h>
 #include <string.h>
 #include <time.h>
+#include <syslog.h>
+#include <assert.h>
 #include <openssl/rand.h>
 #include <openssl/rsa.h>
 #include <openssl/engine.h>
@@ -23,6 +25,7 @@
 #include <openssl/pem.h>
 #include <openssl/buffer.h>
 #include <openssl/err.h>
+#include <openssl/evp.h>
 #include <libcitadel.h>
 
 
@@ -332,35 +335,35 @@ 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) {
-               fprintf(stderr, "dkim: this message cannot be signed because it has no body\n");
+               syslog(LOG_DEBUG, "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)
-       //fprintf(stderr, "Supplied headers:\n-----\n%s-----\n", ChrPtr(header_block));
+       //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
-       //fprintf(stderr, "Body:\n-----\n%s-----\n", ChrPtr(email));
+       //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);
-       //fprintf(stderr, "Canonicalized body:\n-----\n%s-----\n", relaxed_body);
+       //syslog(LOG_DEBUG, "Canonicalized body:\n-----\n%s-----\n", relaxed_body);
 
        // hash of the canonicalized body
-       unsigned char *uhash = malloc(SHA256_DIGEST_LENGTH);
-       SHA256((unsigned char*)relaxed_body, relaxed_body_len, uhash);
-       free(relaxed_body);                                                     // don't need this anymore
+       unsigned char *body_hash = malloc(SHA256_DIGEST_LENGTH);
+       SHA256((unsigned char*)relaxed_body, relaxed_body_len, body_hash);
+       free(relaxed_body);                                                     // all we need now is the hash
        relaxed_body = NULL;
 
        // base64 encode the body hash
-       char *encoded_body_hash = dkim_base64_encode(uhash, SHA256_DIGEST_LENGTH);
-       free(uhash);
+       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=
-       // fprintf(stderr, "Body hash: %s\n", encoded_body_hash);
+       syslog(LOG_DEBUG, "dkim: body hash: %s", encoded_body_hash);
 
        // "relaxed" header canonicalization, step 1 : unfold the headers
        StrBuf *unfolded_headers = NewStrBufDup(header_block);
@@ -373,17 +376,19 @@ 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);
 
-       // fprintf(stderr, "Canonicalized headers:\n-----\n%s-----\n", (char *)ChrPtr(unfolded_headers));
+       //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=%ld; c=relaxed/relaxed; h=%s; bh=%s; b=",
+               "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,
@@ -394,21 +399,23 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
        free(encoded_body_hash);                                                // Don't need this anymore either.
 
        // Add the initial DKIM header (which is still missing the value after "b=") to the headers to be signed.
-       // As far as I can tell, RFC6376 does *not* want us to include any "\r\n" after "b="
+       // RFC6376 3.7 tells us NOT to include CRLF after "b="
        StrBufAppendBufPlain(unfolded_headers, HKEY("dkim-signature:"), 0);
        StrBufAppendBuf(unfolded_headers, dkim_header, 0);
-       // fprintf(stderr, "Canonicalized headers:\n-----\n%s\n-----\n", (char *)ChrPtr(unfolded_headers));
+       //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.
 
-       // Compute a hash of the canonicalized headers.  RFC6376 says that we hash and sign everything up to the "b="
-       // and then we'll add the rest at the end.
-       unsigned char *headers_hash = malloc(SHA256_DIGEST_LENGTH);
-       SHA256((unsigned char*)ChrPtr(unfolded_headers), StrLength(unfolded_headers), headers_hash);
-       FreeStrBuf(&unfolded_headers);                                          // All we need now is the hash.
+       // 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) {
-               fprintf(stderr, "dkim: BIO_new_mem_buf() failed\n");
+               syslog(LOG_ERR, "dkim: BIO_new_mem_buf() failed");
                abort();
        }
 
@@ -421,78 +428,97 @@ void dkim_sign(StrBuf *email, char *pkey_in, char *domain, char *selector) {
                NULL                            // parameter passed to callback or password if callback is NULL
        );
        if (pkey == NULL) {
-               fprintf(stderr, "dkim: PEM_read_bio_PrivateKey() failed\n");
+               syslog(LOG_ERR, "dkim: PEM_read_bio_PrivateKey() failed");
                abort();
        }
        BIO_free(bufio);                        // Don't need this anymore, we have `pkey` now
 
-       // Everything I ever needed to know I learned from https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying
-       EVP_MD_CTX *mdctx = NULL;
+
+       // 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
 
        // Create the Message Digest Context
-       mdctx = EVP_MD_CTX_create();
+       EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
        if (mdctx == NULL) {
-               fprintf(stderr, "dkim: EVP_MD_CTX_create() failed\n");
+               syslog(LOG_ERR, "dkim: EVP_MD_CTX_create() failed with error 0x%lx", ERR_get_error());
                abort();
        }
 
        // Initialize the DigestSign operation using SHA-256 algorithm
        if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
-               fprintf(stderr, "dkim: EVP_DigestSignInit() failed\n");
+               syslog(LOG_ERR, "dkim: EVP_DigestSignInit() failed with error 0x%lx", ERR_get_error());
                abort();
        }
 
-       // free the private key (we don't need it past here)
-       EVP_PKEY_free(pkey);
-
        // Call update with the "message" (the canonicalized headers)
-       if (EVP_DigestSignUpdate(mdctx, headers_hash, SHA256_DIGEST_LENGTH) != 1) {
-               fprintf(stderr, "dkim: EVP_DigestSignUpdate() failed\n");
+       // 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();
        }
-       free(headers_hash);
-
-       // Finalize the DigestSign operation.
 
+       // 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) {
-               fprintf(stderr, "dkim: EVP_DigestSignFinal() failed\n");
+               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));
+
        // Allocate memory for the signature based on size in slen
-       unsigned char *sig = NULL;
-       sig = OPENSSL_malloc(slen);
+       unsigned char *sig = OPENSSL_malloc(slen);
        if (sig == NULL) {
-               fprintf(stderr, "dkim: OPENSSL_malloc() failed\n");
+               syslog(LOG_ERR, "dkim: OPENSSL_malloc() failed");
                abort();
        }
 
        // Obtain the signature
        if (EVP_DigestSignFinal(mdctx, sig, &slen) != 1) {
-               fprintf(stderr, "dkim: EVP_DigestSignFinal() failed\n");
+               syslog(LOG_ERR, "dkim: EVP_DigestSignFinal() failed");
                abort();
        }
+
+       // 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);
+       }
+       // End verify
+
+       // ok we're done with all this crap now
        EVP_MD_CTX_free(mdctx);
+       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);
        free(sig);                                                      // Free the raw signature, keep the b64-encoded one.
        
-       // We should probably wrap the dkim signature here.
+       // wrap dkim header
+       // sig_b64 = dkim_wrap(sig_b64, sig_b64_len);
 
-       // Add the signature to the original header block.  Also append the extra CRLF indicating end of headers.
-       StrBufAppendPrintf(header_block, "DKIM-Signature: %s%s\r\n\r\n", (char *)ChrPtr(dkim_header), sig_b64);
+       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.
-
-       // Append the body.
-       StrBufAppendBuf(header_block, email, 0);
-       SwapBuffers(header_block, email);
+       StrBufAppendBuf(output_msg, header_block, 0);
        FreeStrBuf(&header_block);
+       StrBufAppendPrintf(output_msg, "\r\n");
+       StrBufAppendBuf(output_msg, email, 0);
+       SwapBuffers(output_msg, email);
+       FreeStrBuf(&output_msg);
 
        // And we're done!
 }