1 // DKIM bindings to Citadel Server
3 // (C) 2024 by Art Cancro
5 // This program is open source software. Use, duplication, or disclosure is subject to the GNU General Public License v3.
7 // Make sure we don't accidentally use any deprecated API calls
8 #define OPENSSL_NO_DEPRECATED_3_0
18 #include <openssl/rand.h>
19 #include <openssl/rsa.h>
20 #include <openssl/engine.h>
21 #include <openssl/sha.h>
22 #include <openssl/hmac.h>
23 #include <openssl/evp.h>
24 #include <openssl/bio.h>
25 #include <openssl/pem.h>
26 #include <openssl/buffer.h>
27 #include <openssl/err.h>
28 #include <openssl/evp.h>
29 #include <libcitadel.h>
30 #include "../../config.h"
31 #include "../../msgbase.h"
34 // Generate a private key and selector for DKIM if needed. This is called during server startup.
35 void dkim_init(void) {
37 char *dkim_private_key = CtdlGetConfigStr("dkim_private_key");
38 if (!IsEmptyStr(dkim_private_key)) {
39 syslog(LOG_DEBUG, "dkim: private key exists and will continue to be used.");
43 EVP_PKEY *pkey = NULL;
45 ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
48 (EVP_PKEY_keygen_init(ctx) == 1)
49 && (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) == 1)
50 && (EVP_PKEY_keygen(ctx, &pkey) == 1)
52 syslog(LOG_DEBUG, "dkim: generated private key");
53 bio = BIO_new(BIO_s_mem());
55 PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL);
56 char *b64key = malloc(4096);
59 BIO_read_ex(bio, b64key, 4096, &readbytes);
60 b64key[readbytes] = 0;
62 while (nl=strchr(b64key, '\n'), nl) { // convert newlines to underscores
65 CtdlSetConfigStr("dkim_private_key", b64key);
71 EVP_PKEY_CTX_free(ctx);
75 char *dkim_selector = CtdlGetConfigStr("dkim_selector");
77 syslog(LOG_DEBUG, "dkim: selector exists: %s", dkim_selector);
80 // Quick and dirty algorithm to make up a five letter nonsense word as a selector
84 new_selector[i] = (rand() % 26) + 'a';
87 syslog(LOG_DEBUG, "dkim: selector created: %s", new_selector);
88 CtdlSetConfigStr("dkim_selector", new_selector);
93 // If the DKIM key, DKIM selector, or set of signing domains has changed, we need to tell the administrator about it.
94 void dkim_check_advisory(char *inetcfg_in) {
96 // If there is no DKIM ... there is nothing to discuss
97 if (IsEmptyStr(CtdlGetConfigStr("dkim_private_key"))) return;
98 if (IsEmptyStr(CtdlGetConfigStr("dkim_selector"))) return;
100 // We're going to build a hash of the private key, the selector, and all signing domains.
101 // The way we build it doesn't matter, and it doesn't even have to be secure.
102 // This is just to let us know that we have to post an update to the administrator if the hash changes.
104 StrBuf *hashsrc = NewStrBuf();
109 StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_private_key"), strlen(CtdlGetConfigStr("dkim_private_key")), 0);
110 StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_selector"), strlen(CtdlGetConfigStr("dkim_selector")), 0);
112 char *ptr = inetcfg_in;
113 while (ptr && *ptr) {
114 char *sep = strchr(ptr, '|');
115 if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
116 StrBufAppendBufPlain(hashsrc, ptr, sep-ptr, 0);
118 ptr = strchr(ptr, '\n');
122 // make a hash from the string...
123 unsigned char *config_hash = malloc(SHA256_DIGEST_LENGTH);
124 SHA256((unsigned char *)ChrPtr(hashsrc), StrLength(hashsrc), config_hash);
125 FreeStrBuf(&hashsrc);
127 // base64 encode it...
128 char *encoded_config_hash = malloc(SHA256_DIGEST_LENGTH * 2);
129 CtdlEncodeBase64(encoded_config_hash, config_hash, SHA256_DIGEST_LENGTH, BASE64_NO_LINEBREAKS);
130 free(config_hash); // all we need now is the encoded hash
132 // Does it match the saved hash?
133 if ( (IsEmptyStr(CtdlGetConfigStr("dkim_config_hash")))
134 || (strcmp(encoded_config_hash, CtdlGetConfigStr("dkim_config_hash")))
136 // No? Post an Aide notification.
137 StrBuf *message = NewStrBuf();
138 StrBufAppendBufPlain(message, HKEY(
139 "Content-type: text/plain\r\n"
141 "Your domain configuration may have changed.\r\n\r\n"
142 "To allow the DKIM signatures of outbound mail to be verified,\r\n"
143 "please ensure that the following DNS records are created:\r\n"
148 while (ptr && *ptr) {
149 char *sep = strchr(ptr, '|');
150 if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
151 StrBufAppendPrintf(message, "Host name : %s._domainkey.", CtdlGetConfigStr("dkim_selector"));
152 StrBufAppendBufPlain(message, ptr, sep-ptr, 0);
153 StrBufAppendBufPlain(message, HKEY("\r\n"), 0);
154 StrBufAppendPrintf(message, "Record type: TXT\r\n");
155 StrBufAppendBufPlain(message, HKEY("Value : v=DKIM1;k=rsa;p="), 0);
157 // FIXME calculate the public key and add it here
159 StrBufAppendPrintf(message, "\r\n\r\n");
161 ptr = strchr(ptr, '\n');
167 quickie_message("Citadel",
171 ChrPtr(message), // text
172 FMT_RFC822, // format
173 "Confirm your DKIM records" // subject
175 FreeStrBuf(&message);
178 // Save it to the config database so we don't do this except when it changes.
179 CtdlSetConfigStr("dkim_config_hash", encoded_config_hash);
180 free(encoded_config_hash);