]> code.citadel.org Git - citadel.git/blob - citadel/server/modules/smtp/dkim_bindings.c
minor tuning to previous commit
[citadel.git] / citadel / server / modules / smtp / dkim_bindings.c
1 // DKIM bindings to Citadel Server
2 //
3 // (C) 2024 by Art Cancro
4 //
5 // This program is open source software.  Use, duplication, or disclosure is subject to the GNU General Public License v3.
6
7 // Make sure we don't accidentally use any deprecated API calls
8 #define OPENSSL_NO_DEPRECATED_3_0
9
10 #include <stdlib.h>
11 #include <unistd.h>
12 #include <stdio.h>
13 #include <ctype.h>
14 #include <string.h>
15 #include <time.h>
16 #include <assert.h>
17 #include <syslog.h>
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
33
34 // Generate a private key and selector for DKIM if needed.  This is called during server startup.
35 void dkim_init(void) {
36
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.");
40         }
41         else {
42                 EVP_PKEY_CTX *ctx;
43                 EVP_PKEY *pkey = NULL;  
44                 BIO *bio = NULL;
45                 ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
46                 if (ctx) {
47                         if (
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)
51                         ) {
52                                 syslog(LOG_DEBUG, "dkim: generated private key");
53                                 bio = BIO_new(BIO_s_mem());
54                                 if (bio) {
55                                         PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL);
56                                         char *b64key = malloc(4096);
57                                         if (b64key) {
58                                                 size_t readbytes;
59                                                 BIO_read_ex(bio, b64key, 4096, &readbytes);
60                                                 b64key[readbytes] = 0;
61                                                 char *nl = NULL;
62                                                 while (nl=strchr(b64key, '\n'), nl) {           // convert newlines to underscores
63                                                         *nl = '_';
64                                                 }
65                                                 CtdlSetConfigStr("dkim_private_key", b64key);
66                                                 free(b64key);
67                                         }
68                                         free(bio);
69                                 }
70                         }
71                         EVP_PKEY_CTX_free(ctx);
72                 }
73         }
74
75         char *dkim_selector = CtdlGetConfigStr("dkim_selector");
76         if (dkim_selector) {
77                 syslog(LOG_DEBUG, "dkim: selector exists: %s", dkim_selector);
78         }
79         else {
80                 // Quick and dirty algorithm to make up a five letter nonsense word as a selector
81                 char new_selector[6];
82                 int i;
83                 for (i=0; i<5; ++i) {
84                         new_selector[i] = (rand() % 26) + 'a';
85                 }
86                 new_selector[5] = 0;
87                 syslog(LOG_DEBUG, "dkim: selector created: %s", new_selector);
88                 CtdlSetConfigStr("dkim_selector", new_selector);
89         }
90 }
91
92
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) {
95
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;
99
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.
103
104         StrBuf *hashsrc = NewStrBuf();
105         if (!hashsrc) {
106                 return;
107         }
108
109         StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_private_key"), strlen(CtdlGetConfigStr("dkim_private_key")), 0);
110         StrBufAppendBufPlain(hashsrc, CtdlGetConfigStr("dkim_selector"), strlen(CtdlGetConfigStr("dkim_selector")), 0);
111
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);
117                 }
118                 ptr = strchr(ptr, '\n');
119                 if (ptr) ++ptr;
120         }
121
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);
126
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
131
132         // Does it match the saved hash?
133         if (    (IsEmptyStr(CtdlGetConfigStr("dkim_config_hash")))
134                 || (strcmp(encoded_config_hash, CtdlGetConfigStr("dkim_config_hash")))
135         ) {
136                 // No?  Post an Aide notification.
137                 StrBuf *message = NewStrBuf();
138                 StrBufAppendBufPlain(message, HKEY(
139                         "Content-type: text/plain\r\n"
140                         "\r\n\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"
144                         "\r\n"
145                 ),0);
146
147                 ptr = inetcfg_in;
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);
156
157                                 // FIXME calculate the public key and add it here
158
159                                 StrBufAppendPrintf(message, "\r\n\r\n");
160                         }
161                         ptr = strchr(ptr, '\n');
162                         if (ptr) {
163                                 ++ptr;
164                         }
165                 }
166
167                 quickie_message("Citadel",
168                         NULL,                           // from
169                         NULL,                           // to
170                         AIDEROOM,                       // room
171                         ChrPtr(message),                // text
172                         FMT_RFC822,                     // format
173                         "Confirm your DKIM records"     // subject
174                 );
175                 FreeStrBuf(&message);
176         }
177
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);
181 }