]> code.citadel.org Git - citadel.git/blobdiff - citadel/server/modules/smtp/dkim_bindings.c
split dkim into signing and binding modules
[citadel.git] / citadel / server / modules / smtp / dkim_bindings.c
diff --git a/citadel/server/modules/smtp/dkim_bindings.c b/citadel/server/modules/smtp/dkim_bindings.c
new file mode 100644 (file)
index 0000000..e762ecf
--- /dev/null
@@ -0,0 +1,178 @@
+// DKIM bindings to Citadel Server
+//
+// (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
+#define OPENSSL_NO_DEPRECATED_3_0
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <time.h>
+#include <assert.h>
+#include <syslog.h>
+#include <openssl/rand.h>
+#include <openssl/rsa.h>
+#include <openssl/engine.h>
+#include <openssl/sha.h>
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/buffer.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <libcitadel.h>
+#include "../../config.h"
+#include "../../msgbase.h"
+
+
+// Generate a private key and selector for DKIM if needed.  This is called during server startup.
+void dkim_init(void) {
+
+       char *dkim_private_key = CtdlGetConfigStr("dkim_private_key");
+       if (!IsEmptyStr(dkim_private_key)) {
+               syslog(LOG_DEBUG, "dkim: private key exists and will continue to be used.");
+       }
+       else {
+               EVP_PKEY_CTX *ctx;
+               EVP_PKEY *pkey = NULL;  
+               BIO *bio = NULL;
+               ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+               if (ctx) {
+                       if (
+                               (EVP_PKEY_keygen_init(ctx) == 1)
+                               && (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) == 1)
+                               && (EVP_PKEY_keygen(ctx, &pkey) == 1)
+                       ) {
+                               syslog(LOG_DEBUG, "dkim: generated private key");
+                               bio = BIO_new(BIO_s_mem());
+                               if (bio) {
+                                       PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL);
+                                       char *b64key = malloc(4096);
+                                       if (b64key) {
+                                               size_t readbytes;
+                                               BIO_read_ex(bio, b64key, 4096, &readbytes);
+                                               b64key[readbytes] = 0;
+                                               char *nl = NULL;
+                                               while (nl=strchr(b64key, '\n'), nl) {           // convert newlines to underscores
+                                                       *nl = '_';
+                                               }
+                                               CtdlSetConfigStr("dkim_private_key", b64key);
+                                               free(b64key);
+                                       }
+                                       free(bio);
+                               }
+                       }
+                       EVP_PKEY_CTX_free(ctx);
+               }
+       }
+
+       char *dkim_selector = CtdlGetConfigStr("dkim_selector");
+       if (dkim_selector) {
+               syslog(LOG_DEBUG, "dkim: selector exists: %s", dkim_selector);
+       }
+       else {
+               // Quick and dirty algorithm to make up a five letter nonsense word as a selector
+               char new_selector[6];
+               int i;
+               for (i=0; i<5; ++i) {
+                       new_selector[i] = (rand() % 26) + 'a';
+               }
+               new_selector[5] = 0;
+               syslog(LOG_DEBUG, "dkim: selector created: %s", new_selector);
+               CtdlSetConfigStr("dkim_selector", new_selector);
+       }
+}
+
+
+// If the DKIM key, DKIM selector, or set of signing domains has changed, we need to tell the administrator about it.
+void dkim_check_advisory(char *inetcfg_in) {
+
+       // If there is no DKIM ... there is nothing to discuss
+       if (IsEmptyStr(CtdlGetConfigStr("dkim_private_key"))) return;
+       if (IsEmptyStr(CtdlGetConfigStr("dkim_selector"))) return;
+
+       // We're going to build a hash of the private key, the selector, and all signing domains.
+       // The way we build it doesn't matter, and it doesn't even have to be secure.
+       // This is just to let us know that we have to post an update to the administrator if the hash changes.
+
+       StrBuf *hashsrc = NewStrBuf();
+       if (!hashsrc) {
+               return;
+       }
+
+       StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_private_key"), strlen(CtdlGetConfigStr("dkim_private_key")), 0);
+       StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_selector"), strlen(CtdlGetConfigStr("dkim_selector")), 0);
+
+       char *ptr = inetcfg_in;
+       while (ptr && *ptr) {
+               char *sep = strchr(ptr, '|');
+               if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
+                       StrBufAppendBufPlain(hashsrc, ptr, sep-ptr, 0);
+               }
+               ptr = strchr(ptr, '\n');
+               if (ptr) ++ptr;
+       }
+
+       // make a hash from the string...
+       unsigned char *config_hash = malloc(SHA256_DIGEST_LENGTH);
+       SHA256((unsigned char *)ChrPtr(hashsrc), StrLength(hashsrc), config_hash);
+       FreeStrBuf(&hashsrc);
+
+       // base64 encode it...
+       char *encoded_config_hash = malloc(SHA256_DIGEST_LENGTH * 2);
+       CtdlEncodeBase64(encoded_config_hash, config_hash, SHA256_DIGEST_LENGTH, BASE64_NO_LINEBREAKS);
+       free(config_hash);                                                      // all we need now is the encoded hash
+
+       // Does it match the saved hash?
+       if (    (IsEmptyStr(CtdlGetConfigStr("dkim_config_hash")))
+               || (strcmp(encoded_config_hash, CtdlGetConfigStr("dkim_config_hash")))
+       ) {
+               // No?  Post an Aide notification.
+               StrBuf *message = NewStrBuf();
+               StrBufAppendBufPlain(message, HKEY(
+                       "Content-type: text/plain\r\n"
+                       "\r\n"
+                       "Your domain configuration may have changed.\r\n"
+                       "To allow the DKIM signatures of outbound mail to be verified,\r\n"
+                       "please ensure that the following DNS records are created:\r\n"
+                       "\r\n"
+               ),0);
+
+               ptr = inetcfg_in;
+               while (ptr && *ptr) {
+                       char *sep = strchr(ptr, '|');
+                       if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
+                               StrBufAppendPrintf(message, "Host name  : %s._domainkey.", CtdlGetConfigStr("dkim_selector"));
+                               StrBufAppendBufPlain(message, ptr, sep-ptr, 0);
+                               StrBufAppendBufPlain(message, HKEY("\r\n"), 0);
+                               StrBufAppendPrintf(message, "Record type: TXT\r\n");
+                               StrBufAppendBufPlain(message, HKEY("Value      : v=DKIM1;k=rsa;p="), 0);
+
+                               // figure out the public key and get it
+
+                               StrBufAppendPrintf(message, "\r\n");
+                       }
+                       if (ptr) ++ptr;
+               }
+
+               quickie_message("Citadel",
+                       NULL,                           // from
+                       NULL,                           // to
+                       AIDEROOM,                       // room
+                       ChrPtr(message),                // text
+                       FMT_RFC822,                     // format
+                       "DKIM records"                  // subject
+               );
+               FreeStrBuf(&message);
+       }
+
+       // Save it to the config database so we don't do this except when it changes.
+       CtdlSetConfigStr("dkim_config_hash", encoded_config_hash);
+       free(encoded_config_hash);
+}