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"
32 #include "smtp_util.h"
35 // Generate a private key and selector for DKIM if needed. This is called during server startup.
36 void dkim_init(void) {
38 char *dkim_private_key = CtdlGetConfigStr("dkim_private_key");
39 if (!IsEmptyStr(dkim_private_key)) {
40 syslog(LOG_DEBUG, "dkim: private key exists and will continue to be used.");
44 EVP_PKEY *pkey = NULL;
46 ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
49 (EVP_PKEY_keygen_init(ctx) == 1)
50 && (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) == 1)
51 && (EVP_PKEY_keygen(ctx, &pkey) == 1)
53 syslog(LOG_DEBUG, "dkim: generated private key");
54 bio = BIO_new(BIO_s_mem());
56 PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL);
57 char *b64key = malloc(4096);
60 BIO_read_ex(bio, b64key, 4096, &readbytes);
61 b64key[readbytes] = 0;
63 while (nl=strchr(b64key, '\n'), nl) { // convert newlines to underscores
66 CtdlSetConfigStr("dkim_private_key", b64key);
72 EVP_PKEY_CTX_free(ctx);
76 char *dkim_selector = CtdlGetConfigStr("dkim_selector");
78 syslog(LOG_DEBUG, "dkim: selector exists: %s", dkim_selector);
81 // Quick and dirty algorithm to make up a five letter nonsense word as a selector
85 new_selector[i] = (rand() % 26) + 'a';
88 syslog(LOG_DEBUG, "dkim: selector created: %s", new_selector);
89 CtdlSetConfigStr("dkim_selector", new_selector);
94 // If the DKIM key, DKIM selector, or set of signing domains has changed, we need to tell the administrator about it.
95 void dkim_check_advisory(char *inetcfg_in) {
97 // If there is no DKIM ... there is nothing to discuss
98 if (IsEmptyStr(CtdlGetConfigStr("dkim_private_key"))) return;
99 if (IsEmptyStr(CtdlGetConfigStr("dkim_selector"))) return;
101 // We're going to build a hash of the private key, the selector, and all signing domains.
102 // The way we build it doesn't matter, and it doesn't even have to be secure.
103 // This is just to let us know that we have to post an update to the administrator if the hash changes.
105 StrBuf *hashsrc = NewStrBuf();
110 StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_private_key"), strlen(CtdlGetConfigStr("dkim_private_key")), 0);
111 StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_selector"), strlen(CtdlGetConfigStr("dkim_selector")), 0);
113 char *ptr = inetcfg_in;
114 while (ptr && *ptr) {
115 char *sep = strchr(ptr, '|');
116 if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
117 StrBufAppendBufPlain(hashsrc, ptr, sep-ptr, 0);
119 ptr = strchr(ptr, '\n');
123 // make a hash from the string...
124 unsigned char *config_hash = malloc(SHA256_DIGEST_LENGTH);
125 SHA256((unsigned char *)ChrPtr(hashsrc), StrLength(hashsrc), config_hash);
126 FreeStrBuf(&hashsrc);
128 // base64 encode it...
129 char *encoded_config_hash = malloc(SHA256_DIGEST_LENGTH * 2);
130 CtdlEncodeBase64(encoded_config_hash, config_hash, SHA256_DIGEST_LENGTH, BASE64_NO_LINEBREAKS);
131 free(config_hash); // all we need now is the encoded hash
133 // Does it match the saved hash?
134 if ( (IsEmptyStr(CtdlGetConfigStr("dkim_config_hash")))
135 || (strcmp(encoded_config_hash, CtdlGetConfigStr("dkim_config_hash")))
137 // No? Post an Aide notification.
138 StrBuf *message = NewStrBuf();
139 StrBufAppendBufPlain(message, HKEY(
140 "Content-type: text/plain\r\n"
142 "Your domain configuration may have changed.\r\n\r\n"
143 "To allow the DKIM signatures of outbound mail to be verified,\r\n"
144 "please ensure that the following DNS records are created:\r\n"
149 EVP_PKEY *pkey = dkim_import_key(CtdlGetConfigStr("dkim_private_key"));
151 pubkey = dkim_get_public_key(pkey);
157 while (ptr && *ptr) {
158 char *sep = strchr(ptr, '|');
159 if (sep && !strncasecmp(sep+1, HKEY("localhost"))) {
160 StrBufAppendPrintf(message, "Host name : %s._domainkey.", CtdlGetConfigStr("dkim_selector"));
161 StrBufAppendBufPlain(message, ptr, sep-ptr, 0);
162 StrBufAppendBufPlain(message, HKEY("\r\n"), 0);
163 StrBufAppendPrintf(message, "Record type: TXT\r\n");
164 StrBufAppendPrintf(message, "Value : v=DKIM1;k=rsa;p=%s\r\n", pubkey);
165 StrBufAppendPrintf(message, "\r\n");
167 ptr = strchr(ptr, '\n');
175 StrBufAppendBufPlain(message, HKEY("YOW! Something went wrong.\r\n\r\n"), 0);
178 quickie_message("Citadel",
182 ChrPtr(message), // text
183 FMT_RFC822, // format
184 "Confirm your DKIM records" // subject
186 FreeStrBuf(&message);
189 // Save it to the config database so we don't do this except when it changes.
190 CtdlSetConfigStr("dkim_config_hash", encoded_config_hash);
191 free(encoded_config_hash);