Commit | Line | Data |
---|---|---|
fc2ba559 | 1 | /* $Cambridge: exim/src/src/dkim-exim.c,v 1.4 2008/09/30 10:03:55 tom Exp $ */ |
f7572e5a TK |
2 | |
3 | /************************************************* | |
4 | * Exim - an Internet mail transport agent * | |
5 | *************************************************/ | |
6 | ||
7 | /* Copyright (c) University of Cambridge 1995 - 2007 */ | |
8 | /* See the file NOTICE for conditions of use and distribution. */ | |
9 | ||
10 | /* Code for DKIM support. Other DKIM relevant code is in | |
11 | receive.c, transport.c and transports/smtp.c */ | |
12 | ||
13 | #include "exim.h" | |
14 | ||
15 | #ifdef EXPERIMENTAL_DKIM | |
16 | ||
17 | /* Globals related to the DKIM reference library. */ | |
18 | DKIMContext *dkim_context = NULL; | |
19 | DKIMSignOptions *dkim_sign_options = NULL; | |
20 | DKIMVerifyOptions *dkim_verify_options = NULL; | |
21 | int dkim_verify_result = DKIM_NEUTRAL; | |
22 | int dkim_internal_status = DKIM_SUCCESS; | |
23 | ||
24 | /* Global char buffer for getc/ungetc functions. We need | |
25 | to accumulate some chars to be able to match EOD and | |
26 | doubled SMTP dots. Those must not be fed to the validation | |
27 | engine. */ | |
28 | int dkimbuff[6] = {256,256,256,256,256,256}; | |
29 | ||
30 | /* receive_getc() wrapper that feeds DKIM while Exim reads | |
31 | the message. */ | |
32 | int dkim_receive_getc(void) { | |
33 | int i; | |
34 | ||
35 | #ifdef EXPERIMENTAL_DOMAINKEYS | |
36 | int c = dk_receive_getc(); | |
37 | #else | |
38 | int c = receive_getc(); | |
39 | #endif | |
40 | ||
41 | if ((dkim_context != NULL) && | |
42 | (dkim_internal_status == DKIM_SUCCESS)) { | |
43 | /* Send oldest byte */ | |
44 | if (dkimbuff[0] < 256) { | |
45 | DKIMVerifyProcess(dkim_context,(char *)&dkimbuff[0],1); | |
46 | /* debug_printf("%c",(int)dkimbuff[0]); */ | |
47 | } | |
48 | /* rotate buffer */ | |
49 | for (i=0;i<5;i++) dkimbuff[i]=dkimbuff[i+1]; | |
50 | dkimbuff[5]=c; | |
51 | /* look for our candidate patterns */ | |
52 | if ( (dkimbuff[1] == '\r') && | |
53 | (dkimbuff[2] == '\n') && | |
54 | (dkimbuff[3] == '.') && | |
55 | (dkimbuff[4] == '\r') && | |
56 | (dkimbuff[5] == '\n') ) { | |
57 | /* End of DATA */ | |
58 | dkimbuff[1] = 256; | |
59 | dkimbuff[2] = 256; | |
60 | dkimbuff[3] = 256; | |
61 | dkimbuff[4] = 256; | |
62 | dkimbuff[5] = 256; | |
63 | } | |
64 | if ( (dkimbuff[2] == '\r') && | |
65 | (dkimbuff[3] == '\n') && | |
66 | (dkimbuff[4] == '.') && | |
67 | (dkimbuff[5] == '.') ) { | |
68 | /* doubled dot, skip this char */ | |
69 | dkimbuff[5] = 256; | |
70 | } | |
71 | } | |
72 | ||
73 | return c; | |
74 | } | |
75 | ||
76 | /* When exim puts a char back in the fd, we | |
77 | must rotate our buffer back. */ | |
78 | int dkim_receive_ungetc(int c) { | |
79 | ||
80 | if ((dkim_context != NULL) && | |
81 | (dkim_internal_status == DKIM_SUCCESS)) { | |
82 | int i; | |
83 | /* rotate buffer back */ | |
84 | for (i=5;i>0;i--) dkimbuff[i]=dkimbuff[i-1]; | |
85 | dkimbuff[0]=256; | |
86 | } | |
87 | ||
88 | #ifdef EXPERIMENTAL_DOMAINKEYS | |
89 | return dk_receive_ungetc(c); | |
90 | #else | |
91 | return receive_ungetc(c); | |
92 | #endif | |
93 | } | |
94 | ||
95 | ||
96 | void dkim_exim_verify_init(void) { | |
97 | int old_pool = store_pool; | |
98 | ||
99 | /* Bail out unless we got perfect conditions */ | |
100 | if (!(smtp_input && | |
101 | !smtp_batched_input && | |
102 | dkim_do_verify)) { | |
103 | return; | |
104 | } | |
105 | ||
106 | store_pool = POOL_PERM; | |
107 | ||
108 | dkim_context = NULL; | |
109 | dkim_verify_options = NULL; | |
110 | ||
111 | dkim_context = store_get(sizeof(DKIMContext)); | |
112 | dkim_verify_options = store_get(sizeof(DKIMVerifyOptions)); | |
113 | ||
114 | if (!dkim_context || | |
115 | !dkim_verify_options) { | |
116 | debug_printf("DKIM: Can't allocate memory for verifying.\n"); | |
117 | dkim_context = NULL; | |
118 | } | |
119 | ||
120 | memset(dkim_context,0,sizeof(DKIMContext)); | |
121 | memset(dkim_verify_options,0,sizeof(DKIMVerifyOptions)); | |
122 | ||
123 | dkim_verify_options->nHonorBodyLengthTag = 1; /* Honor the l= tag */ | |
124 | dkim_verify_options->nCheckPolicy = 1; /* Fetch sender's policy */ | |
125 | dkim_verify_options->nSubjectRequired = 1; /* Do not require Subject header inclusion */ | |
126 | ||
127 | dkim_verify_options->pfnSelectorCallback = NULL; | |
128 | dkim_verify_options->pfnPolicyCallback = NULL; | |
129 | ||
130 | dkim_status_wrap( DKIMVerifyInit(dkim_context, dkim_verify_options), | |
131 | "error calling DKIMVerifyInit()" ); | |
132 | ||
133 | if (dkim_internal_status != DKIM_SUCCESS) { | |
134 | /* Invalidate context */ | |
135 | dkim_context = NULL; | |
136 | } | |
137 | ||
138 | store_pool = old_pool; | |
139 | } | |
140 | ||
141 | ||
142 | void dkim_exim_verify_finish(void) { | |
143 | int i; | |
144 | int old_pool = store_pool; | |
145 | ||
146 | if (!dkim_do_verify || | |
147 | (!(smtp_input && !smtp_batched_input)) || | |
148 | (dkim_context == NULL) || | |
149 | (dkim_internal_status != DKIM_SUCCESS)) return; | |
150 | ||
151 | store_pool = POOL_PERM; | |
152 | ||
153 | /* Flush eventual remaining input chars */ | |
154 | for (i=0;i<6;i++) | |
155 | if (dkimbuff[i] < 256) | |
156 | DKIMVerifyProcess(dkim_context,(char *)&dkimbuff[i],1); | |
157 | ||
158 | /* Fetch global result. Can be one of: | |
159 | DKIM_SUCCESS | |
160 | DKIM_PARTIAL_SUCCESS | |
161 | DKIM_NEUTRAL | |
162 | DKIM_FAIL | |
163 | */ | |
164 | dkim_verify_result = DKIMVerifyResults(dkim_context); | |
165 | ||
166 | store_pool = old_pool; | |
167 | } | |
168 | ||
169 | ||
170 | /* Lookup result for a given domain (or identity) */ | |
171 | int dkim_exim_verify_result(uschar *domain, uschar **result, uschar **error) { | |
172 | int sig_count = 0; | |
173 | int i,rc; | |
174 | char policy[512]; | |
175 | DKIMVerifyDetails *dkim_verify_details = NULL; | |
176 | ||
177 | if (!dkim_do_verify || | |
178 | (!(smtp_input && !smtp_batched_input)) || | |
179 | (dkim_context == NULL) || | |
180 | (dkim_internal_status != DKIM_SUCCESS)) { | |
181 | rc = DKIM_EXIM_UNVERIFIED; | |
182 | goto YIELD; | |
183 | } | |
184 | ||
185 | DKIMVerifyGetDetails(dkim_context, | |
186 | &sig_count, | |
187 | &dkim_verify_details, | |
188 | policy); | |
189 | ||
190 | ||
191 | rc = DKIM_EXIM_UNSIGNED; | |
192 | ||
193 | debug_printf("DKIM: We have %d signature(s)\n",sig_count); | |
194 | for (i=0;i<sig_count;i++) { | |
195 | debug_printf( "DKIM: [%d] ", i + 1 ); | |
196 | if (!dkim_verify_details[i].Domain) { | |
197 | debug_printf("parse error (no domain)\n"); | |
198 | continue; | |
199 | } | |
200 | ||
201 | if (dkim_verify_details[i].nResult >= 0) { | |
202 | debug_printf( "GOOD d=%s i=%s\n", | |
203 | dkim_verify_details[i].Domain, | |
204 | dkim_verify_details[i].IdentityDomain ); | |
205 | } | |
206 | else { | |
207 | debug_printf( "FAIL d=%s i=%s c=%d\n", | |
208 | dkim_verify_details[i].Domain, | |
209 | dkim_verify_details[i].IdentityDomain, | |
210 | dkim_verify_details[i].nResult | |
211 | ); | |
212 | ||
213 | } | |
214 | ||
215 | if ( (strcmpic(domain,dkim_verify_details[i].Domain) == 0) || | |
216 | (strcmpic(domain,dkim_verify_details[i].IdentityDomain) == 0) ) { | |
217 | if (dkim_verify_details[i].nResult >= 0) { | |
218 | rc = DKIM_EXIM_GOOD; | |
219 | /* TODO: Add From: domain check */ | |
220 | } | |
221 | else { | |
222 | /* Return DEFER for temp. error types */ | |
223 | if (dkim_verify_details[i].nResult == DKIM_SELECTOR_DNS_TEMP_FAILURE) { | |
224 | rc = DKIM_EXIM_DEFER; | |
225 | } | |
226 | else { | |
227 | rc = DKIM_EXIM_FAIL; | |
228 | } | |
229 | } | |
230 | } | |
231 | } | |
232 | ||
233 | YIELD: | |
234 | switch (rc) { | |
235 | case DKIM_EXIM_FAIL: | |
236 | *result = "bad"; | |
237 | break; | |
238 | case DKIM_EXIM_DEFER: | |
239 | *result = "defer"; | |
240 | break; | |
241 | case DKIM_EXIM_UNVERIFIED: | |
242 | *result = "unverified"; | |
243 | break; | |
244 | case DKIM_EXIM_UNSIGNED: | |
245 | *result = "unsigned"; | |
246 | break; | |
247 | case DKIM_EXIM_GOOD: | |
248 | *result = "good"; | |
249 | break; | |
250 | } | |
251 | ||
252 | return rc; | |
253 | } | |
254 | ||
255 | ||
256 | ||
257 | uschar *dkim_exim_sign_headers = NULL; | |
258 | int dkim_exim_header_callback(const char* header) { | |
259 | int sep = 0; | |
260 | uschar *hdr_ptr = dkim_exim_sign_headers; | |
261 | uschar *hdr_itr = NULL; | |
262 | uschar hdr_buf[512]; | |
263 | uschar *hdr_name = string_copy(US header); | |
264 | char *colon_pos = strchr(hdr_name,':'); | |
265 | ||
266 | if (colon_pos == NULL) return 0; | |
267 | *colon_pos = '\0'; | |
268 | ||
269 | debug_printf("DKIM: header '%s' ",hdr_name); | |
270 | while ((hdr_itr = string_nextinlist(&hdr_ptr, &sep, | |
271 | hdr_buf, | |
272 | sizeof(hdr_buf))) != NULL) { | |
273 | if (strcmpic((uschar *)hdr_name,hdr_itr) == 0) { | |
274 | debug_printf("included in signature.\n"); | |
275 | return 1; | |
276 | } | |
277 | } | |
278 | debug_printf("NOT included in signature.\n"); | |
279 | return 0; | |
280 | } | |
281 | ||
282 | uschar *dkim_exim_sign(int dkim_fd, | |
283 | uschar *dkim_private_key, | |
284 | uschar *dkim_domain, | |
285 | uschar *dkim_selector, | |
286 | uschar *dkim_canon, | |
287 | uschar *dkim_sign_headers) { | |
288 | ||
289 | uschar *rc = NULL; | |
290 | char buf[4096]; | |
291 | int seen_lf = 0; | |
292 | int seen_lfdot = 0; | |
293 | int save_errno = 0; | |
294 | int sread; | |
295 | char *signature; | |
296 | int old_pool = store_pool; | |
297 | store_pool = POOL_PERM; | |
298 | ||
299 | dkim_context = NULL; | |
300 | dkim_sign_options = NULL; | |
301 | ||
302 | dkim_context = store_get(sizeof(DKIMContext)); | |
303 | dkim_sign_options = store_get(sizeof(DKIMSignOptions)); | |
304 | ||
ca15ec2f TK |
305 | memset(dkim_sign_options,0,sizeof(DKIMSignOptions)); |
306 | memset(dkim_context,0,sizeof(DKIMContext)); | |
307 | ||
f7572e5a TK |
308 | dkim_sign_options->nIncludeBodyLengthTag = 0; |
309 | dkim_sign_options->nIncludeCopiedHeaders = 0; | |
310 | dkim_sign_options->nHash = DKIM_HASH_SHA256; | |
311 | dkim_sign_options->nIncludeTimeStamp = 0; | |
312 | dkim_sign_options->nIncludeQueryMethod = 0; | |
313 | dkim_sign_options->pfnHeaderCallback = dkim_exim_header_callback; | |
314 | dkim_sign_options->nIncludeBodyHash = DKIM_BODYHASH_IETF_1; | |
315 | ||
316 | ||
317 | dkim_domain = expand_string(dkim_domain); | |
318 | if (dkim_domain == NULL) { | |
319 | /* expansion error, do not send message. */ | |
320 | log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " | |
321 | "dkim_domain: %s", expand_string_message); | |
322 | rc = NULL; | |
323 | goto CLEANUP; | |
324 | } | |
325 | /* Set up $dkim_domain expansion variable. */ | |
326 | dkim_signing_domain = dkim_domain; | |
327 | Ustrncpy((uschar *)dkim_sign_options->szDomain,dkim_domain,255); | |
328 | ||
329 | ||
330 | /* Get selector to use. */ | |
331 | dkim_selector = expand_string(dkim_selector); | |
332 | if (dkim_selector == NULL) { | |
333 | log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " | |
334 | "dkim_selector: %s", expand_string_message); | |
335 | rc = NULL; | |
336 | goto CLEANUP; | |
337 | } | |
338 | /* Set up $dkim_selector expansion variable. */ | |
339 | dkim_signing_selector = dkim_selector; | |
340 | Ustrncpy((uschar *)dkim_sign_options->szSelector,dkim_selector,79); | |
341 | ||
342 | /* Expand provided options */ | |
343 | dkim_canon = expand_string(dkim_canon?dkim_canon:US"relaxed"); | |
344 | if (dkim_canon == NULL) { | |
345 | /* expansion error, do not send message. */ | |
346 | log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " | |
347 | "dkim_canon: %s", expand_string_message); | |
348 | rc = NULL; | |
349 | goto CLEANUP; | |
350 | } | |
351 | if (Ustrcmp(dkim_canon, "relaxed") == 0) | |
352 | dkim_sign_options->nCanon = DKIM_SIGN_RELAXED; | |
353 | else if (Ustrcmp(dkim_canon, "simple") == 0) | |
354 | dkim_sign_options->nCanon = DKIM_SIGN_SIMPLE; | |
355 | else { | |
356 | log_write(0, LOG_MAIN, "DKIM: unknown canonicalization method '%s', defaulting to 'relaxed'.\n",dkim_canon); | |
357 | dkim_sign_options->nCanon = DKIM_SIGN_RELAXED; | |
358 | } | |
359 | ||
360 | /* Expand signing headers once */ | |
361 | if (dkim_sign_headers != NULL) { | |
362 | dkim_sign_headers = expand_string(dkim_sign_headers); | |
363 | if (dkim_sign_headers == NULL) { | |
364 | log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " | |
365 | "dkim_sign_headers: %s", expand_string_message); | |
366 | rc = NULL; | |
367 | goto CLEANUP; | |
368 | } | |
369 | } | |
370 | ||
371 | if (dkim_sign_headers == NULL) { | |
372 | /* Use RFC defaults */ | |
373 | dkim_sign_headers = US"from:sender:reply-to:subject:date:" | |
374 | "message-id:to:cc:mime-version:content-type:" | |
375 | "content-transfer-encoding:content-id:" | |
376 | "content-description:resent-date:resent-from:" | |
377 | "resent-sender:resent-to:resent-cc:resent-message-id:" | |
378 | "in-reply-to:references:" | |
379 | "list-id:list-help:list-unsubscribe:" | |
380 | "list-subscribe:list-post:list-owner:list-archive"; | |
381 | } | |
382 | dkim_exim_sign_headers = dkim_sign_headers; | |
383 | ||
384 | /* Get private key to use. */ | |
385 | dkim_private_key = expand_string(dkim_private_key); | |
386 | if (dkim_private_key == NULL) { | |
387 | log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " | |
388 | "dkim_private_key: %s", expand_string_message); | |
389 | rc = NULL; | |
390 | goto CLEANUP; | |
391 | } | |
392 | ||
393 | if ( (Ustrlen(dkim_private_key) == 0) || | |
394 | (Ustrcmp(dkim_private_key,"0") == 0) || | |
395 | (Ustrcmp(dkim_private_key,"false") == 0) ) { | |
396 | /* don't sign, but no error */ | |
397 | rc = US""; | |
398 | goto CLEANUP; | |
399 | } | |
400 | ||
401 | if (dkim_private_key[0] == '/') { | |
402 | int privkey_fd = 0; | |
403 | /* Looks like a filename, load the private key. */ | |
404 | memset(big_buffer,0,big_buffer_size); | |
405 | privkey_fd = open(CS dkim_private_key,O_RDONLY); | |
406 | (void)read(privkey_fd,big_buffer,16383); | |
407 | (void)close(privkey_fd); | |
408 | dkim_private_key = big_buffer; | |
409 | } | |
410 | ||
411 | /* Initialize signing context. */ | |
412 | dkim_status_wrap( DKIMSignInit(dkim_context, dkim_sign_options), | |
413 | "error calling DKIMSignInit()" ); | |
414 | ||
415 | if (dkim_internal_status != DKIM_SUCCESS) { | |
416 | /* Invalidate context */ | |
417 | dkim_context = NULL; | |
418 | goto CLEANUP; | |
419 | } | |
420 | ||
421 | while((sread = read(dkim_fd,&buf,4096)) > 0) { | |
422 | int pos = 0; | |
423 | char c; | |
424 | ||
425 | while (pos < sread) { | |
426 | c = buf[pos++]; | |
427 | ||
428 | if ((c == '.') && seen_lfdot) { | |
429 | /* escaped dot, write "\n.", continue */ | |
430 | dkim_internal_status = DKIMSignProcess(dkim_context,"\n.",2); | |
431 | seen_lf = 0; | |
432 | seen_lfdot = 0; | |
433 | continue; | |
434 | } | |
435 | ||
436 | if (seen_lfdot) { | |
437 | /* EOM, write "\n" and break */ | |
438 | dkim_internal_status = DKIMSignProcess(dkim_context,"\n",1); | |
439 | break; | |
440 | } | |
441 | ||
442 | if ((c == '.') && seen_lf) { | |
443 | seen_lfdot = 1; | |
444 | continue; | |
445 | } | |
446 | ||
447 | if (seen_lf) { | |
448 | /* normal lf, just send it */ | |
449 | dkim_internal_status = DKIMSignProcess(dkim_context,"\n",1); | |
450 | seen_lf = 0; | |
451 | } | |
452 | ||
453 | if (c == '\n') { | |
454 | seen_lf = 1; | |
455 | continue; | |
456 | } | |
457 | ||
458 | /* write the char */ | |
459 | dkim_internal_status = DKIMSignProcess(dkim_context,&c,1); | |
460 | } | |
461 | } | |
462 | ||
463 | /* Handle failed read above. */ | |
464 | if (sread == -1) { | |
465 | debug_printf("DKIM: Error reading -K file.\n"); | |
466 | save_errno = errno; | |
467 | rc = NULL; | |
468 | goto CLEANUP; | |
469 | } | |
470 | ||
471 | if (!dkim_status_wrap(dkim_internal_status, | |
472 | "error while processing message data")) { | |
473 | rc = NULL; | |
474 | goto CLEANUP; | |
475 | } | |
476 | ||
477 | if (!dkim_status_wrap( DKIMSignGetSig2( dkim_context, dkim_private_key, &signature ), | |
478 | "error while signing message" ) ) { | |
479 | rc = NULL; | |
480 | goto CLEANUP; | |
481 | } | |
482 | ||
483 | log_write(0, LOG_MAIN, "Message signed with DKIM: %s\n",signature); | |
484 | ||
485 | rc = store_get(strlen(signature)+3); | |
486 | Ustrcpy(rc,US signature); | |
487 | Ustrcat(rc,US"\r\n"); | |
488 | ||
489 | CLEANUP: | |
490 | if (dkim_context != NULL) { | |
491 | dkim_context = NULL; | |
492 | } | |
493 | store_pool = old_pool; | |
494 | errno = save_errno; | |
495 | return rc; | |
496 | } | |
497 | ||
498 | unsigned int dkim_status_wrap(int stat, uschar *text) { | |
499 | char *p = DKIMGetErrorString(stat); | |
500 | ||
501 | if (stat != DKIM_SUCCESS) { | |
502 | debug_printf("DKIM: %s",text?text:US""); | |
503 | if (p) debug_printf(" (%s)",p); | |
504 | debug_printf("\n"); | |
505 | } | |
506 | dkim_internal_status = stat; | |
507 | return (dkim_internal_status==DKIM_SUCCESS)?1:0; | |
508 | } | |
509 | ||
510 | #endif |