Commit | Line | Data |
---|---|---|
617d3932 JH |
1 | /************************************************* |
2 | * Exim - an Internet mail transport agent * | |
3 | *************************************************/ | |
4 | /* Experimental ARC support for Exim | |
5 | Copyright (c) Jeremy Harris 2018 | |
6 | License: GPL | |
7 | */ | |
8 | ||
9 | #include "exim.h" | |
7d4064fb | 10 | #if defined EXPERIMENTAL_ARC |
2b2cfa83 | 11 | # if defined DISABLE_DKIM |
617d3932 JH |
12 | # error DKIM must also be enabled for ARC |
13 | # else | |
14 | ||
15 | # include "functions.h" | |
16 | # include "pdkim/pdkim.h" | |
17 | # include "pdkim/signing.h" | |
18 | ||
19 | extern pdkim_ctx * dkim_verify_ctx; | |
20 | extern pdkim_ctx dkim_sign_ctx; | |
21 | ||
a93dbf44 | 22 | #define ARC_SIGN_OPT_TSTAMP BIT(0) |
0e83c148 JH |
23 | #define ARC_SIGN_OPT_EXPIRE BIT(1) |
24 | ||
25 | #define ARC_SIGN_DEFAULT_EXPIRE_DELTA (60 * 60 * 24 * 30) /* one month */ | |
a93dbf44 | 26 | |
617d3932 JH |
27 | /******************************************************************************/ |
28 | ||
29 | typedef struct hdr_rlist { | |
30 | struct hdr_rlist * prev; | |
31 | BOOL used; | |
32 | header_line * h; | |
33 | } hdr_rlist; | |
34 | ||
35 | typedef struct arc_line { | |
36 | header_line * complete; /* including the header name; nul-term */ | |
37 | uschar * relaxed; | |
38 | ||
39 | /* identified tag contents */ | |
40 | /*XXX t= for AS? */ | |
41 | blob i; | |
42 | blob cv; | |
43 | blob a; | |
44 | blob b; | |
45 | blob bh; | |
46 | blob d; | |
47 | blob h; | |
48 | blob s; | |
49 | blob c; | |
50 | blob l; | |
51 | ||
52 | /* tag content sub-portions */ | |
53 | blob a_algo; | |
54 | blob a_hash; | |
55 | ||
56 | blob c_head; | |
57 | blob c_body; | |
58 | ||
59 | /* modified copy of b= field in line */ | |
60 | blob rawsig_no_b_val; | |
61 | } arc_line; | |
62 | ||
63 | typedef struct arc_set { | |
64 | struct arc_set * next; | |
65 | struct arc_set * prev; | |
66 | ||
67 | unsigned instance; | |
68 | arc_line * hdr_aar; | |
69 | arc_line * hdr_ams; | |
70 | arc_line * hdr_as; | |
71 | ||
93c931f8 | 72 | const uschar * ams_verify_done; |
617d3932 JH |
73 | BOOL ams_verify_passed; |
74 | } arc_set; | |
75 | ||
76 | typedef struct arc_ctx { | |
77 | arc_set * arcset_chain; | |
78 | arc_set * arcset_chain_last; | |
79 | } arc_ctx; | |
80 | ||
81 | #define ARC_HDR_AAR US"ARC-Authentication-Results:" | |
82 | #define ARC_HDRLEN_AAR 27 | |
83 | #define ARC_HDR_AMS US"ARC-Message-Signature:" | |
84 | #define ARC_HDRLEN_AMS 22 | |
85 | #define ARC_HDR_AS US"ARC-Seal:" | |
86 | #define ARC_HDRLEN_AS 9 | |
87 | #define HDR_AR US"Authentication-Results:" | |
88 | #define HDRLEN_AR 23 | |
89 | ||
0e83c148 JH |
90 | static time_t now; |
91 | static time_t expire; | |
617d3932 JH |
92 | static hdr_rlist * headers_rlist; |
93 | static arc_ctx arc_sign_ctx = { NULL }; | |
9cffa436 | 94 | static arc_ctx arc_verify_ctx = { NULL }; |
617d3932 JH |
95 | |
96 | ||
97 | /******************************************************************************/ | |
98 | ||
99 | ||
100 | /* Get the instance number from the header. | |
101 | Return 0 on error */ | |
102 | static unsigned | |
103 | arc_instance_from_hdr(const arc_line * al) | |
104 | { | |
978e78de | 105 | const uschar * s = al->i.data; |
617d3932 | 106 | if (!s || !al->i.len) return 0; |
978e78de | 107 | return (unsigned) atoi(CCS s); |
617d3932 JH |
108 | } |
109 | ||
110 | ||
111 | static uschar * | |
112 | skip_fws(uschar * s) | |
113 | { | |
114 | uschar c = *s; | |
115 | while (c && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) c = *++s; | |
116 | return s; | |
117 | } | |
118 | ||
119 | ||
120 | /* Locate instance struct on chain, inserting a new one if | |
121 | needed. The chain is in increasing-instance-number order | |
122 | by the "next" link, and we have a "prev" link also. | |
123 | */ | |
124 | ||
125 | static arc_set * | |
126 | arc_find_set(arc_ctx * ctx, unsigned i) | |
127 | { | |
128 | arc_set ** pas, * as, * next, * prev; | |
129 | ||
130 | for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain; | |
131 | as = *pas; pas = &as->next) | |
132 | { | |
133 | if (as->instance > i) break; | |
134 | if (as->instance == i) | |
135 | { | |
136 | DEBUG(D_acl) debug_printf("ARC: existing instance %u\n", i); | |
137 | return as; | |
138 | } | |
139 | next = as->next; | |
140 | prev = as; | |
141 | } | |
142 | ||
143 | DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i); | |
f3ebb786 | 144 | *pas = as = store_get(sizeof(arc_set), FALSE); |
617d3932 JH |
145 | memset(as, 0, sizeof(arc_set)); |
146 | as->next = next; | |
147 | as->prev = prev; | |
148 | as->instance = i; | |
149 | if (next) | |
150 | next->prev = as; | |
151 | else | |
152 | ctx->arcset_chain_last = as; | |
153 | return as; | |
154 | } | |
155 | ||
156 | ||
157 | ||
158 | /* Insert a tag content into the line structure. | |
159 | Note this is a reference to existing data, not a copy. | |
160 | Check for already-seen tag. | |
161 | The string-pointer is on the '=' for entry. Update it past the | |
162 | content (to the ;) on return; | |
163 | */ | |
164 | ||
165 | static uschar * | |
166 | arc_insert_tagvalue(arc_line * al, unsigned loff, uschar ** ss) | |
167 | { | |
168 | uschar * s = *ss; | |
169 | uschar c = *++s; | |
170 | blob * b = (blob *)(US al + loff); | |
171 | size_t len = 0; | |
172 | ||
173 | /* [FWS] tag-value [FWS] */ | |
174 | ||
175 | if (b->data) return US"fail"; | |
176 | s = skip_fws(s); /* FWS */ | |
177 | ||
178 | b->data = s; | |
179 | while ((c = *s) && c != ';') { len++; s++; } | |
180 | *ss = s; | |
181 | while (len && ((c = s[-1]) == ' ' || c == '\t' || c == '\n' || c == '\r')) | |
182 | { s--; len--; } /* FWS */ | |
183 | b->len = len; | |
184 | return NULL; | |
185 | } | |
186 | ||
187 | ||
188 | /* Inspect a header line, noting known tag fields. | |
189 | Check for duplicates. */ | |
190 | ||
191 | static uschar * | |
192 | arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only) | |
193 | { | |
194 | uschar * s = h->text + off; | |
978e78de | 195 | uschar * r = NULL; /* compiler-quietening */ |
617d3932 JH |
196 | uschar c; |
197 | ||
198 | al->complete = h; | |
199 | ||
200 | if (!instance_only) | |
201 | { | |
f3ebb786 | 202 | al->rawsig_no_b_val.data = store_get(h->slen + 1, TRUE); /* tainted */ |
617d3932 JH |
203 | memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */ |
204 | r = al->rawsig_no_b_val.data + off; | |
205 | al->rawsig_no_b_val.len = off; | |
206 | } | |
207 | ||
208 | /* tag-list = tag-spec *( ";" tag-spec ) [ ";" ] */ | |
209 | ||
210 | while ((c = *s)) | |
211 | { | |
212 | char tagchar; | |
213 | uschar * t; | |
214 | unsigned i = 0; | |
215 | uschar * fieldstart = s; | |
216 | uschar * bstart = NULL, * bend; | |
217 | ||
218 | /* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */ | |
219 | ||
220 | s = skip_fws(s); /* FWS */ | |
221 | if (!*s) break; | |
222 | /* debug_printf("%s: consider '%s'\n", __FUNCTION__, s); */ | |
223 | tagchar = *s++; | |
224 | s = skip_fws(s); /* FWS */ | |
225 | if (!*s) break; | |
226 | ||
227 | if (!instance_only || tagchar == 'i') switch (tagchar) | |
228 | { | |
229 | case 'a': /* a= AMS algorithm */ | |
230 | { | |
231 | if (*s != '=') return US"no 'a' value"; | |
232 | if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup"; | |
233 | ||
234 | /* substructure: algo-hash (eg. rsa-sha256) */ | |
235 | ||
236 | t = al->a_algo.data = al->a.data; | |
237 | while (*t != '-') | |
238 | if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value"; | |
239 | al->a_algo.len = i; | |
240 | if (*t++ != '-') return US"no '-' in 'a' value"; | |
241 | al->a_hash.data = t; | |
242 | al->a_hash.len = al->a.len - i - 1; | |
243 | } | |
244 | break; | |
245 | case 'b': | |
246 | { | |
247 | gstring * g = NULL; | |
248 | ||
249 | switch (*s) | |
250 | { | |
251 | case '=': /* b= AMS signature */ | |
252 | if (al->b.data) return US"already b data"; | |
253 | bstart = s+1; | |
254 | ||
255 | /* The signature can have FWS inserted in the content; | |
256 | make a stripped copy */ | |
257 | ||
258 | while ((c = *++s) && c != ';') | |
259 | if (c != ' ' && c != '\t' && c != '\n' && c != '\r') | |
260 | g = string_catn(g, s, 1); | |
b4dd15a7 | 261 | if (!g) return US"no b= value"; |
617d3932 JH |
262 | al->b.data = string_from_gstring(g); |
263 | al->b.len = g->ptr; | |
e59797e3 | 264 | gstring_release_unused(g); |
617d3932 JH |
265 | bend = s; |
266 | break; | |
267 | case 'h': /* bh= AMS body hash */ | |
268 | s = skip_fws(++s); /* FWS */ | |
269 | if (*s != '=') return US"no bh value"; | |
270 | if (al->bh.data) return US"already bh data"; | |
271 | ||
272 | /* The bodyhash can have FWS inserted in the content; | |
273 | make a stripped copy */ | |
274 | ||
275 | while ((c = *++s) && c != ';') | |
276 | if (c != ' ' && c != '\t' && c != '\n' && c != '\r') | |
277 | g = string_catn(g, s, 1); | |
b4dd15a7 | 278 | if (!g) return US"no bh= value"; |
617d3932 JH |
279 | al->bh.data = string_from_gstring(g); |
280 | al->bh.len = g->ptr; | |
e59797e3 | 281 | gstring_release_unused(g); |
617d3932 JH |
282 | break; |
283 | default: | |
284 | return US"b? tag"; | |
285 | } | |
286 | } | |
287 | break; | |
288 | case 'c': | |
289 | switch (*s) | |
290 | { | |
291 | case '=': /* c= AMS canonicalisation */ | |
292 | if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup"; | |
293 | ||
294 | /* substructure: head/body (eg. relaxed/simple)) */ | |
295 | ||
296 | t = al->c_head.data = al->c.data; | |
297 | while (isalpha(*t)) | |
298 | if (!*t++ || ++i > al->a.len) break; | |
299 | al->c_head.len = i; | |
300 | if (*t++ == '/') /* /body is optional */ | |
301 | { | |
302 | al->c_body.data = t; | |
303 | al->c_body.len = al->c.len - i - 1; | |
304 | } | |
305 | else | |
306 | { | |
307 | al->c_body.data = US"simple"; | |
308 | al->c_body.len = 6; | |
309 | } | |
310 | break; | |
311 | case 'v': /* cv= AS validity */ | |
312 | if (*++s != '=') return US"cv tag val"; | |
313 | if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) return US"cv tag dup"; | |
314 | break; | |
315 | default: | |
316 | return US"c? tag"; | |
317 | } | |
318 | break; | |
319 | case 'd': /* d= AMS domain */ | |
320 | if (*s != '=') return US"d tag val"; | |
321 | if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) return US"d tag dup"; | |
322 | break; | |
323 | case 'h': /* h= AMS headers */ | |
324 | if (*s != '=') return US"h tag val"; | |
325 | if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) return US"h tag dup"; | |
326 | break; | |
327 | case 'i': /* i= ARC set instance */ | |
328 | if (*s != '=') return US"i tag val"; | |
329 | if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) return US"i tag dup"; | |
330 | if (instance_only) goto done; | |
331 | break; | |
332 | case 'l': /* l= bodylength */ | |
333 | if (*s != '=') return US"l tag val"; | |
334 | if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) return US"l tag dup"; | |
335 | break; | |
336 | case 's': /* s= AMS selector */ | |
337 | if (*s != '=') return US"s tag val"; | |
338 | if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) return US"s tag dup"; | |
339 | break; | |
340 | } | |
341 | ||
342 | while ((c = *s) && c != ';') s++; | |
343 | if (c) s++; /* ; after tag-spec */ | |
344 | ||
345 | /* for all but the b= tag, copy the field including FWS. For the b=, | |
346 | drop the tag content. */ | |
347 | ||
348 | if (!instance_only) | |
349 | if (bstart) | |
350 | { | |
351 | size_t n = bstart - fieldstart; | |
352 | memcpy(r, fieldstart, n); /* FWS "b=" */ | |
353 | r += n; | |
354 | al->rawsig_no_b_val.len += n; | |
355 | n = s - bend; | |
356 | memcpy(r, bend, n); /* FWS ";" */ | |
357 | r += n; | |
358 | al->rawsig_no_b_val.len += n; | |
359 | } | |
360 | else | |
361 | { | |
362 | size_t n = s - fieldstart; | |
363 | memcpy(r, fieldstart, n); | |
364 | r += n; | |
365 | al->rawsig_no_b_val.len += n; | |
366 | } | |
367 | } | |
368 | ||
369 | if (!instance_only) | |
370 | *r = '\0'; | |
371 | ||
372 | done: | |
373 | /* debug_printf("%s: finshed\n", __FUNCTION__); */ | |
374 | return NULL; | |
375 | } | |
376 | ||
377 | ||
378 | /* Insert one header line in the correct set of the chain, | |
379 | adding instances as needed and checking for duplicate lines. | |
380 | */ | |
381 | ||
382 | static uschar * | |
383 | arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff, | |
e9dfcfb7 | 384 | BOOL instance_only, arc_line ** alp_ret) |
617d3932 | 385 | { |
ef262e31 | 386 | unsigned i; |
617d3932 | 387 | arc_set * as; |
f3ebb786 | 388 | arc_line * al = store_get(sizeof(arc_line), FALSE), ** alp; |
617d3932 JH |
389 | uschar * e; |
390 | ||
391 | memset(al, 0, sizeof(arc_line)); | |
392 | ||
393 | if ((e = arc_parse_line(al, h, off, instance_only))) | |
394 | { | |
395 | DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e); | |
396 | return US"line parse"; | |
397 | } | |
398 | if (!(i = arc_instance_from_hdr(al))) return US"instance find"; | |
ef262e31 | 399 | if (i > 50) return US"overlarge instance number"; |
617d3932 JH |
400 | if (!(as = arc_find_set(ctx, i))) return US"set find"; |
401 | if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr"; | |
402 | ||
403 | *alp = al; | |
e9dfcfb7 | 404 | if (alp_ret) *alp_ret = al; |
617d3932 JH |
405 | return NULL; |
406 | } | |
407 | ||
408 | ||
409 | ||
410 | ||
411 | static const uschar * | |
412 | arc_try_header(arc_ctx * ctx, header_line * h, BOOL instance_only) | |
413 | { | |
414 | const uschar * e; | |
415 | ||
416 | /*debug_printf("consider hdr '%s'\n", h->text);*/ | |
417 | if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0) | |
418 | { | |
419 | DEBUG(D_acl) | |
420 | { | |
421 | int len = h->slen; | |
422 | uschar * s; | |
423 | for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) | |
424 | s--, len--; | |
425 | debug_printf("ARC: found AAR: %.*s\n", len, h->text); | |
426 | } | |
427 | if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar), | |
e9dfcfb7 | 428 | TRUE, NULL))) |
617d3932 JH |
429 | { |
430 | DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e); | |
431 | return US"inserting AAR"; | |
432 | } | |
433 | } | |
434 | else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0) | |
435 | { | |
436 | arc_line * ams; | |
437 | ||
438 | DEBUG(D_acl) | |
439 | { | |
440 | int len = h->slen; | |
441 | uschar * s; | |
442 | for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) | |
443 | s--, len--; | |
444 | debug_printf("ARC: found AMS: %.*s\n", len, h->text); | |
445 | } | |
446 | if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams), | |
e9dfcfb7 | 447 | instance_only, &ams))) |
617d3932 JH |
448 | { |
449 | DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e); | |
450 | return US"inserting AMS"; | |
451 | } | |
452 | ||
453 | /* defaults */ | |
617d3932 JH |
454 | if (!ams->c.data) |
455 | { | |
456 | ams->c_head.data = US"simple"; ams->c_head.len = 6; | |
457 | ams->c_body = ams->c_head; | |
458 | } | |
459 | } | |
460 | else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0) | |
461 | { | |
462 | DEBUG(D_acl) | |
463 | { | |
464 | int len = h->slen; | |
465 | uschar * s; | |
466 | for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) | |
467 | s--, len--; | |
468 | debug_printf("ARC: found AS: %.*s\n", len, h->text); | |
469 | } | |
470 | if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as), | |
e9dfcfb7 | 471 | instance_only, NULL))) |
617d3932 JH |
472 | { |
473 | DEBUG(D_acl) debug_printf("inserting AS: %s\n", e); | |
474 | return US"inserting AS"; | |
475 | } | |
476 | } | |
477 | return NULL; | |
478 | } | |
479 | ||
480 | ||
481 | ||
482 | /* Gather the chain of arc sets from the headers. | |
483 | Check for duplicates while that is done. Also build the | |
484 | reverse-order headers list; | |
485 | ||
486 | Return: ARC state if determined, eg. by lack of any ARC chain. | |
487 | */ | |
488 | ||
489 | static const uschar * | |
490 | arc_vfy_collect_hdrs(arc_ctx * ctx) | |
491 | { | |
492 | header_line * h; | |
978e78de | 493 | hdr_rlist * r = NULL, * rprev = NULL; |
617d3932 JH |
494 | const uschar * e; |
495 | ||
496 | DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n"); | |
497 | for (h = header_list; h; h = h->next) | |
498 | { | |
f3ebb786 | 499 | r = store_get(sizeof(hdr_rlist), FALSE); |
617d3932 JH |
500 | r->prev = rprev; |
501 | r->used = FALSE; | |
502 | r->h = h; | |
503 | rprev = r; | |
504 | ||
505 | if ((e = arc_try_header(ctx, h, FALSE))) | |
93c931f8 JH |
506 | { |
507 | arc_state_reason = string_sprintf("collecting headers: %s", e); | |
508 | return US"fail"; | |
509 | } | |
617d3932 JH |
510 | } |
511 | headers_rlist = r; | |
512 | ||
513 | if (!ctx->arcset_chain) return US"none"; | |
514 | return NULL; | |
515 | } | |
516 | ||
517 | ||
518 | static BOOL | |
519 | arc_cv_match(arc_line * al, const uschar * s) | |
520 | { | |
521 | return Ustrncmp(s, al->cv.data, al->cv.len) == 0; | |
522 | } | |
523 | ||
524 | /******************************************************************************/ | |
525 | ||
526 | /* Return the hash of headers from the message that the AMS claims it | |
527 | signed. | |
528 | */ | |
529 | ||
530 | static void | |
531 | arc_get_verify_hhash(arc_ctx * ctx, arc_line * ams, blob * hhash) | |
532 | { | |
533 | const uschar * headernames = string_copyn(ams->h.data, ams->h.len); | |
534 | const uschar * hn; | |
535 | int sep = ':'; | |
536 | hdr_rlist * r; | |
537 | BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0; | |
538 | int hashtype = pdkim_hashname_to_hashtype( | |
539 | ams->a_hash.data, ams->a_hash.len); | |
540 | hctx hhash_ctx; | |
541 | const uschar * s; | |
542 | int len; | |
543 | ||
6f47da8d JH |
544 | if ( hashtype == -1 |
545 | || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) | |
617d3932 JH |
546 | { |
547 | DEBUG(D_acl) | |
548 | debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); | |
549 | return; | |
550 | } | |
551 | ||
552 | /* For each headername in the list from the AMS (walking in order) | |
553 | walk the message headers in reverse order, adding to the hash any | |
554 | found for the first time. For that last point, maintain used-marks | |
555 | on the list of message headers. */ | |
556 | ||
557 | DEBUG(D_acl) debug_printf("ARC: AMS header data for verification:\n"); | |
558 | ||
559 | for (r = headers_rlist; r; r = r->prev) | |
560 | r->used = FALSE; | |
561 | while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) | |
562 | for (r = headers_rlist; r; r = r->prev) | |
563 | if ( !r->used | |
564 | && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0 | |
565 | ) | |
566 | { | |
567 | if (relaxed) s = pdkim_relax_header_n(s, r->h->slen, TRUE); | |
568 | ||
569 | len = Ustrlen(s); | |
570 | DEBUG(D_acl) pdkim_quoteprint(s, len); | |
571 | exim_sha_update(&hhash_ctx, s, Ustrlen(s)); | |
572 | r->used = TRUE; | |
573 | break; | |
574 | } | |
575 | ||
d9604f37 | 576 | /* Finally add in the signature header (with the b= tag stripped); no CRLF */ |
617d3932 JH |
577 | |
578 | s = ams->rawsig_no_b_val.data, len = ams->rawsig_no_b_val.len; | |
579 | if (relaxed) | |
d9604f37 | 580 | len = Ustrlen(s = pdkim_relax_header_n(s, len, FALSE)); |
617d3932 JH |
581 | DEBUG(D_acl) pdkim_quoteprint(s, len); |
582 | exim_sha_update(&hhash_ctx, s, len); | |
583 | ||
584 | exim_sha_finish(&hhash_ctx, hhash); | |
585 | DEBUG(D_acl) | |
586 | { debug_printf("ARC: header hash: "); pdkim_hexprint(hhash->data, hhash->len); } | |
587 | return; | |
588 | } | |
589 | ||
590 | ||
591 | ||
592 | ||
593 | static pdkim_pubkey * | |
594 | arc_line_to_pubkey(arc_line * al) | |
595 | { | |
596 | uschar * dns_txt; | |
597 | pdkim_pubkey * p; | |
598 | ||
599 | if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s", | |
d70fc283 | 600 | (int)al->s.len, al->s.data, (int)al->d.len, al->d.data)))) |
617d3932 JH |
601 | { |
602 | DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n"); | |
603 | return NULL; | |
604 | } | |
605 | ||
606 | if ( !(p = pdkim_parse_pubkey_record(dns_txt)) | |
607 | || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) | |
608 | ) | |
609 | { | |
610 | DEBUG(D_acl) debug_printf("pubkey dns lookup format error\n"); | |
611 | return NULL; | |
612 | } | |
613 | ||
614 | /* If the pubkey limits use to specified hashes, reject unusable | |
615 | signatures. XXX should we have looked for multiple dns records? */ | |
616 | ||
617 | if (p->hashes) | |
618 | { | |
619 | const uschar * list = p->hashes, * ele; | |
620 | int sep = ':'; | |
621 | ||
622 | while ((ele = string_nextinlist(&list, &sep, NULL, 0))) | |
623 | if (Ustrncmp(ele, al->a_hash.data, al->a_hash.len) == 0) break; | |
624 | if (!ele) | |
625 | { | |
626 | DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n", | |
978e78de | 627 | p->hashes, (int)al->a.len, al->a.data); |
617d3932 JH |
628 | return NULL; |
629 | } | |
630 | } | |
631 | return p; | |
632 | } | |
633 | ||
634 | ||
635 | ||
636 | ||
637 | static pdkim_bodyhash * | |
638 | arc_ams_setup_vfy_bodyhash(arc_line * ams) | |
639 | { | |
6f47da8d | 640 | int canon_head = -1, canon_body = -1; |
617d3932 JH |
641 | long bodylen; |
642 | ||
b3d9ebf5 | 643 | if (!ams->c.data) ams->c.data = US"simple"; /* RFC 6376 (DKIM) default */ |
617d3932 JH |
644 | pdkim_cstring_to_canons(ams->c.data, ams->c.len, &canon_head, &canon_body); |
645 | bodylen = ams->l.data | |
646 | ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) : -1; | |
647 | ||
648 | return pdkim_set_bodyhash(dkim_verify_ctx, | |
649 | pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len), | |
650 | canon_body, | |
651 | bodylen); | |
652 | } | |
653 | ||
654 | ||
655 | ||
656 | /* Verify an AMS. This is a DKIM-sig header, but with an ARC i= tag | |
657 | and without a DKIM v= tag. | |
658 | */ | |
659 | ||
93c931f8 | 660 | static const uschar * |
617d3932 JH |
661 | arc_ams_verify(arc_ctx * ctx, arc_set * as) |
662 | { | |
663 | arc_line * ams = as->hdr_ams; | |
664 | pdkim_bodyhash * b; | |
665 | pdkim_pubkey * p; | |
666 | blob sighash; | |
667 | blob hhash; | |
668 | ev_ctx vctx; | |
669 | int hashtype; | |
670 | const uschar * errstr; | |
671 | ||
93c931f8 | 672 | as->ams_verify_done = US"in-progress"; |
617d3932 JH |
673 | |
674 | /* Check the AMS has all the required tags: | |
675 | "a=" algorithm | |
676 | "b=" signature | |
677 | "bh=" body hash | |
678 | "d=" domain (for key lookup) | |
679 | "h=" headers (included in signature) | |
680 | "s=" key-selector (for key lookup) | |
681 | */ | |
682 | if ( !ams->a.data || !ams->b.data || !ams->bh.data || !ams->d.data | |
683 | || !ams->h.data || !ams->s.data) | |
93c931f8 JH |
684 | { |
685 | as->ams_verify_done = arc_state_reason = US"required tag missing"; | |
617d3932 | 686 | return US"fail"; |
93c931f8 | 687 | } |
617d3932 JH |
688 | |
689 | ||
690 | /* The bodyhash should have been created earlier, and the dkim code should | |
691 | have managed calculating it during message input. Find the reference to it. */ | |
692 | ||
693 | if (!(b = arc_ams_setup_vfy_bodyhash(ams))) | |
93c931f8 JH |
694 | { |
695 | as->ams_verify_done = arc_state_reason = US"internal hash setup error"; | |
617d3932 | 696 | return US"fail"; |
93c931f8 | 697 | } |
617d3932 JH |
698 | |
699 | DEBUG(D_acl) | |
700 | { | |
701 | debug_printf("ARC i=%d AMS Body bytes hashed: %lu\n" | |
702 | " Body %.*s computed: ", | |
703 | as->instance, b->signed_body_bytes, | |
978e78de | 704 | (int)ams->a_hash.len, ams->a_hash.data); |
617d3932 JH |
705 | pdkim_hexprint(CUS b->bh.data, b->bh.len); |
706 | } | |
707 | ||
708 | /* We know the bh-tag blob is of a nul-term string, so safe as a string */ | |
709 | ||
710 | if ( !ams->bh.data | |
711 | || (pdkim_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) | |
712 | || memcmp(sighash.data, b->bh.data, b->bh.len) != 0 | |
713 | ) | |
714 | { | |
715 | DEBUG(D_acl) | |
716 | { | |
717 | debug_printf("ARC i=%d AMS Body hash from headers: ", as->instance); | |
718 | pdkim_hexprint(sighash.data, sighash.len); | |
719 | debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance); | |
720 | } | |
93c931f8 | 721 | return as->ams_verify_done = arc_state_reason = US"AMS body hash miscompare"; |
617d3932 JH |
722 | } |
723 | ||
724 | DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance); | |
725 | ||
726 | /* Get the public key from DNS */ | |
727 | ||
728 | if (!(p = arc_line_to_pubkey(ams))) | |
93c931f8 | 729 | return as->ams_verify_done = arc_state_reason = US"pubkey problem"; |
617d3932 JH |
730 | |
731 | /* We know the b-tag blob is of a nul-term string, so safe as a string */ | |
732 | pdkim_decode_base64(ams->b.data, &sighash); | |
733 | ||
734 | arc_get_verify_hhash(ctx, ams, &hhash); | |
735 | ||
736 | /* Setup the interface to the signing library */ | |
737 | ||
738 | if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx))) | |
739 | { | |
740 | DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); | |
93c931f8 | 741 | as->ams_verify_done = arc_state_reason = US"internal sigverify init error"; |
617d3932 JH |
742 | return US"fail"; |
743 | } | |
744 | ||
745 | hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len); | |
6f47da8d JH |
746 | if (hashtype == -1) |
747 | { | |
748 | DEBUG(D_acl) debug_printf("ARC i=%d AMS verify bad a_hash\n", as->instance); | |
749 | return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; | |
750 | } | |
617d3932 JH |
751 | |
752 | if ((errstr = exim_dkim_verify(&vctx, | |
753 | pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash))) | |
754 | { | |
755 | DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr); | |
93c931f8 | 756 | return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; |
617d3932 JH |
757 | } |
758 | ||
759 | DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance); | |
760 | as->ams_verify_passed = TRUE; | |
761 | return NULL; | |
762 | } | |
763 | ||
764 | ||
765 | ||
766 | /* Check the sets are instance-continuous and that all | |
767 | members are present. Check that no arc_seals are "fail". | |
768 | Set the highest instance number global. | |
769 | Verify the latest AMS. | |
770 | */ | |
771 | static uschar * | |
772 | arc_headers_check(arc_ctx * ctx) | |
773 | { | |
774 | arc_set * as; | |
775 | int inst; | |
776 | BOOL ams_fail_found = FALSE; | |
617d3932 | 777 | |
ea7b1f16 | 778 | if (!(as = ctx->arcset_chain_last)) |
617d3932 JH |
779 | return US"none"; |
780 | ||
ea7b1f16 | 781 | for(inst = as->instance; as; as = as->prev, inst--) |
617d3932 | 782 | { |
ea7b1f16 JH |
783 | if (as->instance != inst) |
784 | arc_state_reason = string_sprintf("i=%d (sequence; expected %d)", | |
785 | as->instance, inst); | |
786 | else if (!as->hdr_aar || !as->hdr_ams || !as->hdr_as) | |
787 | arc_state_reason = string_sprintf("i=%d (missing header)", as->instance); | |
788 | else if (arc_cv_match(as->hdr_as, US"fail")) | |
789 | arc_state_reason = string_sprintf("i=%d (cv)", as->instance); | |
790 | else | |
791 | goto good; | |
792 | ||
793 | DEBUG(D_acl) debug_printf("ARC chain fail at %s\n", arc_state_reason); | |
794 | return US"fail"; | |
617d3932 | 795 | |
ea7b1f16 | 796 | good: |
617d3932 JH |
797 | /* Evaluate the oldest-pass AMS validation while we're here. |
798 | It does not affect the AS chain validation but is reported as | |
799 | auxilary info. */ | |
800 | ||
801 | if (!ams_fail_found) | |
802 | if (arc_ams_verify(ctx, as)) | |
803 | ams_fail_found = TRUE; | |
804 | else | |
805 | arc_oldest_pass = inst; | |
93c931f8 | 806 | arc_state_reason = NULL; |
617d3932 | 807 | } |
ea7b1f16 JH |
808 | if (inst != 0) |
809 | { | |
810 | arc_state_reason = string_sprintf("(sequence; expected i=%d)", inst); | |
811 | DEBUG(D_acl) debug_printf("ARC chain fail %s\n", arc_state_reason); | |
812 | return US"fail"; | |
813 | } | |
617d3932 JH |
814 | |
815 | arc_received = ctx->arcset_chain_last; | |
ea7b1f16 | 816 | arc_received_instance = arc_received->instance; |
617d3932 JH |
817 | |
818 | /* We can skip the latest-AMS validation, if we already did it. */ | |
819 | ||
820 | as = ctx->arcset_chain_last; | |
3c676fa8 | 821 | if (!as->ams_verify_passed) |
93c931f8 | 822 | { |
3c676fa8 JH |
823 | if (as->ams_verify_done) |
824 | { | |
825 | arc_state_reason = as->ams_verify_done; | |
826 | return US"fail"; | |
827 | } | |
828 | if (!!arc_ams_verify(ctx, as)) | |
829 | return US"fail"; | |
93c931f8 | 830 | } |
617d3932 JH |
831 | return NULL; |
832 | } | |
833 | ||
834 | ||
835 | /******************************************************************************/ | |
836 | static const uschar * | |
837 | arc_seal_verify(arc_ctx * ctx, arc_set * as) | |
838 | { | |
839 | arc_line * hdr_as = as->hdr_as; | |
840 | arc_set * as2; | |
841 | int hashtype; | |
842 | hctx hhash_ctx; | |
843 | blob hhash_computed; | |
844 | blob sighash; | |
845 | ev_ctx vctx; | |
846 | pdkim_pubkey * p; | |
847 | const uschar * errstr; | |
848 | ||
849 | DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance); | |
850 | /* | |
851 | 1. If the value of the "cv" tag on that seal is "fail", the | |
852 | chain state is "fail" and the algorithm stops here. (This | |
853 | step SHOULD be skipped if the earlier step (2.1) was | |
854 | performed) [it was] | |
855 | ||
856 | 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv | |
857 | == "none" && i != 1)) then the chain state is "fail" and the | |
858 | algorithm stops here (note that the ordering of the logic is | |
859 | structured for short-circuit evaluation). | |
860 | */ | |
861 | ||
862 | if ( as->instance == 1 && !arc_cv_match(hdr_as, US"none") | |
863 | || arc_cv_match(hdr_as, US"none") && as->instance != 1 | |
864 | ) | |
93c931f8 JH |
865 | { |
866 | arc_state_reason = US"seal cv state"; | |
617d3932 | 867 | return US"fail"; |
93c931f8 | 868 | } |
617d3932 JH |
869 | |
870 | /* | |
871 | 3. Initialize a hash function corresponding to the "a" tag of | |
872 | the ARC-Seal. | |
873 | */ | |
874 | ||
875 | hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len); | |
876 | ||
6f47da8d JH |
877 | if ( hashtype == -1 |
878 | || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) | |
617d3932 JH |
879 | { |
880 | DEBUG(D_acl) | |
881 | debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); | |
93c931f8 | 882 | arc_state_reason = US"seal hash setup error"; |
617d3932 JH |
883 | return US"fail"; |
884 | } | |
885 | ||
886 | /* | |
887 | 4. Compute the canonicalized form of the ARC header fields, in | |
888 | the order described in Section 5.4.2, using the "relaxed" | |
889 | header canonicalization defined in Section 3.4.2 of | |
890 | [RFC6376]. Pass the canonicalized result to the hash | |
891 | function. | |
d9604f37 JH |
892 | |
893 | Headers are CRLF-separated, but the last one is not crlf-terminated. | |
617d3932 JH |
894 | */ |
895 | ||
896 | DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n"); | |
897 | for (as2 = ctx->arcset_chain; | |
898 | as2 && as2->instance <= as->instance; | |
899 | as2 = as2->next) | |
900 | { | |
901 | arc_line * al; | |
902 | uschar * s; | |
903 | int len; | |
904 | ||
905 | al = as2->hdr_aar; | |
906 | if (!(s = al->relaxed)) | |
907 | al->relaxed = s = pdkim_relax_header_n(al->complete->text, | |
908 | al->complete->slen, TRUE); | |
909 | len = Ustrlen(s); | |
910 | DEBUG(D_acl) pdkim_quoteprint(s, len); | |
911 | exim_sha_update(&hhash_ctx, s, len); | |
912 | ||
913 | al = as2->hdr_ams; | |
914 | if (!(s = al->relaxed)) | |
915 | al->relaxed = s = pdkim_relax_header_n(al->complete->text, | |
916 | al->complete->slen, TRUE); | |
917 | len = Ustrlen(s); | |
918 | DEBUG(D_acl) pdkim_quoteprint(s, len); | |
919 | exim_sha_update(&hhash_ctx, s, len); | |
920 | ||
921 | al = as2->hdr_as; | |
922 | if (as2->instance == as->instance) | |
923 | s = pdkim_relax_header_n(al->rawsig_no_b_val.data, | |
d9604f37 | 924 | al->rawsig_no_b_val.len, FALSE); |
617d3932 JH |
925 | else if (!(s = al->relaxed)) |
926 | al->relaxed = s = pdkim_relax_header_n(al->complete->text, | |
927 | al->complete->slen, TRUE); | |
928 | len = Ustrlen(s); | |
929 | DEBUG(D_acl) pdkim_quoteprint(s, len); | |
930 | exim_sha_update(&hhash_ctx, s, len); | |
931 | } | |
932 | ||
933 | /* | |
934 | 5. Retrieve the final digest from the hash function. | |
935 | */ | |
936 | ||
937 | exim_sha_finish(&hhash_ctx, &hhash_computed); | |
938 | DEBUG(D_acl) | |
939 | { | |
940 | debug_printf("ARC i=%d AS Header %.*s computed: ", | |
978e78de | 941 | as->instance, (int)hdr_as->a_hash.len, hdr_as->a_hash.data); |
617d3932 JH |
942 | pdkim_hexprint(hhash_computed.data, hhash_computed.len); |
943 | } | |
944 | ||
945 | ||
946 | /* | |
947 | 6. Retrieve the public key identified by the "s" and "d" tags in | |
948 | the ARC-Seal, as described in Section 4.1.6. | |
949 | */ | |
950 | ||
951 | if (!(p = arc_line_to_pubkey(hdr_as))) | |
952 | return US"pubkey problem"; | |
953 | ||
954 | /* | |
955 | 7. Determine whether the signature portion ("b" tag) of the ARC- | |
956 | Seal and the digest computed above are valid according to the | |
957 | public key. (See also Section Section 8.4 for failure case | |
958 | handling) | |
959 | ||
960 | 8. If the signature is not valid, the chain state is "fail" and | |
961 | the algorithm stops here. | |
962 | */ | |
963 | ||
964 | /* We know the b-tag blob is of a nul-term string, so safe as a string */ | |
965 | pdkim_decode_base64(hdr_as->b.data, &sighash); | |
966 | ||
967 | if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx))) | |
968 | { | |
969 | DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); | |
970 | return US"fail"; | |
971 | } | |
972 | ||
617d3932 JH |
973 | if ((errstr = exim_dkim_verify(&vctx, |
974 | pdkim_hashes[hashtype].exim_hashmethod, | |
975 | &hhash_computed, &sighash))) | |
976 | { | |
977 | DEBUG(D_acl) | |
978 | debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr); | |
d9604f37 | 979 | arc_state_reason = US"seal sigverify error"; |
617d3932 JH |
980 | return US"fail"; |
981 | } | |
982 | ||
983 | DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d pass\n", as->instance); | |
984 | return NULL; | |
985 | } | |
986 | ||
987 | ||
988 | static const uschar * | |
989 | arc_verify_seals(arc_ctx * ctx) | |
990 | { | |
5054c4fd | 991 | arc_set * as = ctx->arcset_chain_last; |
617d3932 JH |
992 | |
993 | if (!as) | |
994 | return US"none"; | |
995 | ||
5054c4fd JH |
996 | for ( ; as; as = as->prev) if (arc_seal_verify(ctx, as)) return US"fail"; |
997 | ||
617d3932 JH |
998 | DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n"); |
999 | return NULL; | |
1000 | } | |
1001 | /******************************************************************************/ | |
1002 | ||
1003 | /* Do ARC verification. Called from DATA ACL, on a verify = arc | |
1004 | condition. No arguments; we are checking globals. | |
1005 | ||
1006 | Return: The ARC state, or NULL on error. | |
1007 | */ | |
1008 | ||
1009 | const uschar * | |
1010 | acl_verify_arc(void) | |
1011 | { | |
617d3932 JH |
1012 | const uschar * res; |
1013 | ||
9cffa436 JH |
1014 | memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx)); |
1015 | ||
311fbe57 JH |
1016 | if (!dkim_verify_ctx) |
1017 | { | |
1018 | DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n"); | |
1019 | return NULL; | |
1020 | } | |
1021 | ||
617d3932 JH |
1022 | /* AS evaluation, per |
1023 | https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6 | |
1024 | */ | |
1025 | /* 1. Collect all ARC sets currently on the message. If there were | |
1026 | none, the ARC state is "none" and the algorithm stops here. | |
1027 | */ | |
1028 | ||
9cffa436 | 1029 | if ((res = arc_vfy_collect_hdrs(&arc_verify_ctx))) |
617d3932 JH |
1030 | goto out; |
1031 | ||
1032 | /* 2. If the form of any ARC set is invalid (e.g., does not contain | |
1033 | exactly one of each of the three ARC-specific header fields), | |
1034 | then the chain state is "fail" and the algorithm stops here. | |
1035 | ||
1036 | 1. To avoid the overhead of unnecessary computation and delay | |
1037 | from crypto and DNS operations, the cv value for all ARC- | |
1038 | Seal(s) MAY be checked at this point. If any of the values | |
1039 | are "fail", then the overall state of the chain is "fail" and | |
1040 | the algorithm stops here. | |
1041 | ||
1042 | 3. Conduct verification of the ARC-Message-Signature header field | |
1043 | bearing the highest instance number. If this verification fails, | |
1044 | then the chain state is "fail" and the algorithm stops here. | |
1045 | */ | |
1046 | ||
9cffa436 | 1047 | if ((res = arc_headers_check(&arc_verify_ctx))) |
617d3932 JH |
1048 | goto out; |
1049 | ||
1050 | /* 4. For each ARC-Seal from the "N"th instance to the first, apply the | |
1051 | following logic: | |
1052 | ||
1053 | 1. If the value of the "cv" tag on that seal is "fail", the | |
1054 | chain state is "fail" and the algorithm stops here. (This | |
1055 | step SHOULD be skipped if the earlier step (2.1) was | |
1056 | performed) | |
1057 | ||
1058 | 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv | |
1059 | == "none" && i != 1)) then the chain state is "fail" and the | |
1060 | algorithm stops here (note that the ordering of the logic is | |
1061 | structured for short-circuit evaluation). | |
1062 | ||
1063 | 3. Initialize a hash function corresponding to the "a" tag of | |
1064 | the ARC-Seal. | |
1065 | ||
1066 | 4. Compute the canonicalized form of the ARC header fields, in | |
1067 | the order described in Section 5.4.2, using the "relaxed" | |
1068 | header canonicalization defined in Section 3.4.2 of | |
1069 | [RFC6376]. Pass the canonicalized result to the hash | |
1070 | function. | |
1071 | ||
1072 | 5. Retrieve the final digest from the hash function. | |
1073 | ||
1074 | 6. Retrieve the public key identified by the "s" and "d" tags in | |
1075 | the ARC-Seal, as described in Section 4.1.6. | |
1076 | ||
1077 | 7. Determine whether the signature portion ("b" tag) of the ARC- | |
1078 | Seal and the digest computed above are valid according to the | |
1079 | public key. (See also Section Section 8.4 for failure case | |
1080 | handling) | |
1081 | ||
1082 | 8. If the signature is not valid, the chain state is "fail" and | |
1083 | the algorithm stops here. | |
1084 | ||
1085 | 5. If all seals pass validation, then the chain state is "pass", and | |
1086 | the algorithm is complete. | |
1087 | */ | |
1088 | ||
9cffa436 | 1089 | if ((res = arc_verify_seals(&arc_verify_ctx))) |
617d3932 JH |
1090 | goto out; |
1091 | ||
1092 | res = US"pass"; | |
1093 | ||
1094 | out: | |
1095 | return res; | |
1096 | } | |
1097 | ||
1098 | /******************************************************************************/ | |
1099 | ||
1100 | /* Prepend the header to the rlist */ | |
1101 | ||
1102 | static hdr_rlist * | |
1103 | arc_rlist_entry(hdr_rlist * list, const uschar * s, int len) | |
1104 | { | |
f3ebb786 | 1105 | hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), FALSE); |
617d3932 JH |
1106 | header_line * h = r->h = (header_line *)(r+1); |
1107 | ||
1108 | r->prev = list; | |
1109 | r->used = FALSE; | |
1110 | h->next = NULL; | |
1111 | h->type = 0; | |
1112 | h->slen = len; | |
1113 | h->text = US s; | |
1114 | ||
1115 | /* This works for either NL or CRLF lines; also nul-termination */ | |
1116 | while (*++s) | |
1117 | if (*s == '\n' && s[1] != '\t' && s[1] != ' ') break; | |
1118 | s++; /* move past end of line */ | |
1119 | ||
1120 | return r; | |
1121 | } | |
1122 | ||
1123 | ||
1124 | /* Walk the given headers strings identifying each header, and construct | |
b3d9ebf5 | 1125 | a reverse-order list. |
617d3932 JH |
1126 | */ |
1127 | ||
1128 | static hdr_rlist * | |
1129 | arc_sign_scan_headers(arc_ctx * ctx, gstring * sigheaders) | |
1130 | { | |
1131 | const uschar * s; | |
1132 | hdr_rlist * rheaders = NULL; | |
1133 | ||
1134 | s = sigheaders ? sigheaders->s : NULL; | |
1135 | if (s) while (*s) | |
1136 | { | |
978e78de | 1137 | const uschar * s2 = s; |
617d3932 JH |
1138 | |
1139 | /* This works for either NL or CRLF lines; also nul-termination */ | |
1140 | while (*++s2) | |
1141 | if (*s2 == '\n' && s2[1] != '\t' && s2[1] != ' ') break; | |
1142 | s2++; /* move past end of line */ | |
1143 | ||
1144 | rheaders = arc_rlist_entry(rheaders, s, s2 - s); | |
1145 | s = s2; | |
1146 | } | |
1147 | return rheaders; | |
1148 | } | |
1149 | ||
1150 | ||
1151 | ||
1152 | /* Return the A-R content, without identity, with line-ending and | |
1153 | NUL termination. */ | |
1154 | ||
1155 | static BOOL | |
1156 | arc_sign_find_ar(header_line * headers, const uschar * identity, blob * ret) | |
1157 | { | |
1158 | header_line * h; | |
1159 | int ilen = Ustrlen(identity); | |
1160 | ||
1161 | ret->data = NULL; | |
1162 | for(h = headers; h; h = h->next) | |
1163 | { | |
1164 | uschar * s = h->text, c; | |
1165 | int len = h->slen; | |
1166 | ||
1167 | if (Ustrncmp(s, HDR_AR, HDRLEN_AR) != 0) continue; | |
1168 | s += HDRLEN_AR, len -= HDRLEN_AR; /* header name */ | |
1169 | while ( len > 0 | |
1170 | && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) | |
1171 | s++, len--; /* FWS */ | |
1172 | if (Ustrncmp(s, identity, ilen) != 0) continue; | |
1173 | s += ilen; len -= ilen; /* identity */ | |
1174 | if (len <= 0) continue; | |
1175 | if ((c = *s) && c == ';') s++, len--; /* identity terminator */ | |
1176 | while ( len > 0 | |
1177 | && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) | |
1178 | s++, len--; /* FWS */ | |
1179 | if (len <= 0) continue; | |
1180 | ret->data = s; | |
1181 | ret->len = len; | |
1182 | return TRUE; | |
1183 | } | |
1184 | return FALSE; | |
1185 | } | |
1186 | ||
1187 | ||
1188 | ||
1189 | /* Append a constructed AAR including CRLF. Add it to the arc_ctx too. */ | |
1190 | ||
1191 | static gstring * | |
1192 | arc_sign_append_aar(gstring * g, arc_ctx * ctx, | |
1193 | const uschar * identity, int instance, blob * ar) | |
1194 | { | |
ba5120a4 | 1195 | int aar_off = gstring_length(g); |
f3ebb786 JH |
1196 | arc_set * as = |
1197 | store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), FALSE); | |
617d3932 JH |
1198 | arc_line * al = (arc_line *)(as+1); |
1199 | header_line * h = (header_line *)(al+1); | |
1200 | ||
1201 | g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR); | |
52f12a7c | 1202 | g = string_fmt_append(g, " i=%d; %s;\r\n\t", instance, identity); |
617d3932 JH |
1203 | g = string_catn(g, US ar->data, ar->len); |
1204 | ||
1205 | h->slen = g->ptr - aar_off; | |
1206 | h->text = g->s + aar_off; | |
1207 | al->complete = h; | |
1208 | as->next = NULL; | |
1209 | as->prev = ctx->arcset_chain_last; | |
1210 | as->instance = instance; | |
1211 | as->hdr_aar = al; | |
1212 | if (instance == 1) | |
1213 | ctx->arcset_chain = as; | |
1214 | else | |
1215 | ctx->arcset_chain_last->next = as; | |
1216 | ctx->arcset_chain_last = as; | |
1217 | ||
1218 | DEBUG(D_transport) debug_printf("ARC: AAR '%.*s'\n", h->slen - 2, h->text); | |
1219 | return g; | |
1220 | } | |
1221 | ||
1222 | ||
1223 | ||
1224 | static BOOL | |
1225 | arc_sig_from_pseudoheader(gstring * hdata, int hashtype, const uschar * privkey, | |
1226 | blob * sig, const uschar * why) | |
1227 | { | |
1228 | hashmethod hm = /*sig->keytype == KEYTYPE_ED25519*/ FALSE | |
1229 | ? HASH_SHA2_512 : pdkim_hashes[hashtype].exim_hashmethod; | |
1230 | blob hhash; | |
1231 | es_ctx sctx; | |
1232 | const uschar * errstr; | |
1233 | ||
1234 | DEBUG(D_transport) | |
1235 | { | |
1236 | hctx hhash_ctx; | |
1237 | debug_printf("ARC: %s header data for signing:\n", why); | |
1238 | pdkim_quoteprint(hdata->s, hdata->ptr); | |
1239 | ||
1240 | (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); | |
1241 | exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); | |
1242 | exim_sha_finish(&hhash_ctx, &hhash); | |
1243 | debug_printf("ARC: header hash: "); pdkim_hexprint(hhash.data, hhash.len); | |
1244 | } | |
1245 | ||
1246 | if (FALSE /*need hash for Ed25519 or GCrypt signing*/ ) | |
1247 | { | |
1248 | hctx hhash_ctx; | |
1249 | (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); | |
1250 | exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); | |
1251 | exim_sha_finish(&hhash_ctx, &hhash); | |
1252 | } | |
1253 | else | |
1254 | { | |
1255 | hhash.data = hdata->s; | |
1256 | hhash.len = hdata->ptr; | |
1257 | } | |
1258 | ||
1259 | if ( (errstr = exim_dkim_signing_init(privkey, &sctx)) | |
1260 | || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig))) | |
1261 | { | |
c3d43245 | 1262 | log_write(0, LOG_MAIN, "ARC: %s signing: %s\n", why, errstr); |
c59b09dc JH |
1263 | DEBUG(D_transport) |
1264 | debug_printf("private key, or private-key file content, was: '%s'\n", | |
1265 | privkey); | |
617d3932 JH |
1266 | return FALSE; |
1267 | } | |
1268 | return TRUE; | |
1269 | } | |
1270 | ||
1271 | ||
1272 | ||
1273 | static gstring * | |
1274 | arc_sign_append_sig(gstring * g, blob * sig) | |
1275 | { | |
1276 | /*debug_printf("%s: raw sig ", __FUNCTION__); pdkim_hexprint(sig->data, sig->len);*/ | |
1277 | sig->data = pdkim_encode_base64(sig); | |
1278 | sig->len = Ustrlen(sig->data); | |
1279 | for (;;) | |
1280 | { | |
1281 | int len = MIN(sig->len, 74); | |
1282 | g = string_catn(g, sig->data, len); | |
1283 | if ((sig->len -= len) == 0) break; | |
1284 | sig->data += len; | |
1285 | g = string_catn(g, US"\r\n\t ", 5); | |
1286 | } | |
1287 | g = string_catn(g, US";\r\n", 3); | |
e59797e3 | 1288 | gstring_release_unused(g); |
617d3932 JH |
1289 | string_from_gstring(g); |
1290 | return g; | |
1291 | } | |
1292 | ||
1293 | ||
1294 | /* Append a constructed AMS including CRLF. Add it to the arc_ctx too. */ | |
1295 | ||
1296 | static gstring * | |
1297 | arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance, | |
1298 | const uschar * identity, const uschar * selector, blob * bodyhash, | |
a93dbf44 | 1299 | hdr_rlist * rheaders, const uschar * privkey, unsigned options) |
617d3932 JH |
1300 | { |
1301 | uschar * s; | |
1302 | gstring * hdata = NULL; | |
1303 | int col; | |
1304 | int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ | |
1305 | blob sig; | |
1306 | int ams_off; | |
f3ebb786 | 1307 | arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), FALSE); |
617d3932 JH |
1308 | header_line * h = (header_line *)(al+1); |
1309 | ||
1310 | /* debug_printf("%s\n", __FUNCTION__); */ | |
1311 | ||
1312 | /* Construct the to-be-signed AMS pseudo-header: everything but the sig. */ | |
1313 | ||
1314 | ams_off = g->ptr; | |
52f12a7c JH |
1315 | g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s", |
1316 | ARC_HDR_AMS, instance, identity, selector); /*XXX hardwired a= */ | |
a93dbf44 | 1317 | if (options & ARC_SIGN_OPT_TSTAMP) |
52f12a7c | 1318 | g = string_fmt_append(g, "; t=%lu", (u_long)now); |
0e83c148 | 1319 | if (options & ARC_SIGN_OPT_EXPIRE) |
52f12a7c JH |
1320 | g = string_fmt_append(g, "; x=%lu", (u_long)expire); |
1321 | g = string_fmt_append(g, ";\r\n\tbh=%s;\r\n\th=", | |
1322 | pdkim_encode_base64(bodyhash)); | |
617d3932 JH |
1323 | |
1324 | for(col = 3; rheaders; rheaders = rheaders->prev) | |
1325 | { | |
1326 | const uschar * hnames = US"DKIM-Signature:" PDKIM_DEFAULT_SIGN_HEADERS; | |
1327 | uschar * name, * htext = rheaders->h->text; | |
1328 | int sep = ':'; | |
1329 | ||
1330 | /* Spot headers of interest */ | |
1331 | ||
1332 | while ((name = string_nextinlist(&hnames, &sep, NULL, 0))) | |
1333 | { | |
1334 | int len = Ustrlen(name); | |
1335 | if (strncasecmp(CCS htext, CCS name, len) == 0) | |
1336 | { | |
1337 | /* If too long, fold line in h= field */ | |
1338 | ||
1339 | if (col + len > 78) g = string_catn(g, US"\r\n\t ", 5), col = 3; | |
1340 | ||
1341 | /* Add name to h= list */ | |
1342 | ||
1343 | g = string_catn(g, name, len); | |
1344 | g = string_catn(g, US":", 1); | |
1345 | col += len + 1; | |
1346 | ||
1347 | /* Accumulate header for hashing/signing */ | |
1348 | ||
1349 | hdata = string_cat(hdata, | |
1350 | pdkim_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ | |
1351 | break; | |
1352 | } | |
1353 | } | |
1354 | } | |
1355 | ||
1356 | /* Lose the last colon from the h= list */ | |
1357 | ||
1358 | if (g->s[g->ptr - 1] == ':') g->ptr--; | |
1359 | ||
1360 | g = string_catn(g, US";\r\n\tb=;", 7); | |
1361 | ||
1362 | /* Include the pseudo-header in the accumulation */ | |
617d3932 | 1363 | |
d9604f37 | 1364 | s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE); |
617d3932 JH |
1365 | hdata = string_cat(hdata, s); |
1366 | ||
1367 | /* Calculate the signature from the accumulation */ | |
1368 | /*XXX does that need further relaxation? there are spaces embedded in the b= strings! */ | |
1369 | ||
1370 | if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS")) | |
1371 | return NULL; | |
1372 | ||
1373 | /* Lose the trailing semicolon from the psuedo-header, and append the signature | |
1374 | (folded over lines) and termination to complete it. */ | |
1375 | ||
1376 | g->ptr--; | |
1377 | g = arc_sign_append_sig(g, &sig); | |
1378 | ||
1379 | h->slen = g->ptr - ams_off; | |
1380 | h->text = g->s + ams_off; | |
1381 | al->complete = h; | |
1382 | ctx->arcset_chain_last->hdr_ams = al; | |
1383 | ||
1384 | DEBUG(D_transport) debug_printf("ARC: AMS '%.*s'\n", h->slen - 2, h->text); | |
1385 | return g; | |
1386 | } | |
1387 | ||
1388 | ||
1389 | ||
1390 | /* Look for an arc= result in an A-R header blob. We know that its data | |
1391 | happens to be a NUL-term string. */ | |
1392 | ||
1393 | static uschar * | |
1394 | arc_ar_cv_status(blob * ar) | |
1395 | { | |
1396 | const uschar * resinfo = ar->data; | |
1397 | int sep = ';'; | |
1398 | uschar * methodspec, * s; | |
1399 | ||
1400 | while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0))) | |
1401 | if (Ustrncmp(methodspec, US"arc=", 4) == 0) | |
1402 | { | |
1403 | uschar c; | |
1404 | for (s = methodspec += 4; | |
1405 | (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++; | |
1406 | return string_copyn(methodspec, s - methodspec); | |
1407 | } | |
3d0a6e0f | 1408 | return US"none"; |
617d3932 JH |
1409 | } |
1410 | ||
1411 | ||
1412 | ||
1413 | /* Build the AS header and prepend it */ | |
1414 | ||
1415 | static gstring * | |
1416 | arc_sign_prepend_as(gstring * arcset_interim, arc_ctx * ctx, | |
1417 | int instance, const uschar * identity, const uschar * selector, blob * ar, | |
a93dbf44 | 1418 | const uschar * privkey, unsigned options) |
617d3932 JH |
1419 | { |
1420 | gstring * arcset; | |
617d3932 | 1421 | uschar * status = arc_ar_cv_status(ar); |
f3ebb786 | 1422 | arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), FALSE); |
617d3932 | 1423 | header_line * h = (header_line *)(al+1); |
7af11cd0 | 1424 | uschar * badline_str; |
617d3932 JH |
1425 | |
1426 | gstring * hdata = NULL; | |
1427 | int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ | |
1428 | blob sig; | |
1429 | ||
1430 | /* | |
1431 | - Generate AS | |
1432 | - no body coverage | |
1433 | - no h= tag; implicit coverage | |
1434 | - arc status from A-R | |
1435 | - if fail: | |
1436 | - coverage is just the new ARC set | |
1437 | including self (but with an empty b= in self) | |
1438 | - if non-fail: | |
1439 | - all ARC set headers, set-number order, aar then ams then as, | |
1440 | including self (but with an empty b= in self) | |
1441 | */ | |
7af11cd0 | 1442 | DEBUG(D_transport) debug_printf("ARC: building AS for status '%s'\n", status); |
617d3932 JH |
1443 | |
1444 | /* Construct the AS except for the signature */ | |
1445 | ||
a93dbf44 | 1446 | arcset = string_append(NULL, 9, |
617d3932 JH |
1447 | ARC_HDR_AS, |
1448 | US" i=", string_sprintf("%d", instance), | |
1449 | US"; cv=", status, | |
d4772796 | 1450 | US"; a=rsa-sha256; d=", identity, /*XXX hardwired */ |
a93dbf44 JH |
1451 | US"; s=", selector); /*XXX same as AMS */ |
1452 | if (options & ARC_SIGN_OPT_TSTAMP) | |
1453 | arcset = string_append(arcset, 2, | |
0e83c148 | 1454 | US"; t=", string_sprintf("%lu", (u_long)now)); |
a93dbf44 | 1455 | arcset = string_cat(arcset, |
617d3932 JH |
1456 | US";\r\n\t b=;"); |
1457 | ||
1458 | h->slen = arcset->ptr; | |
1459 | h->text = arcset->s; | |
1460 | al->complete = h; | |
1461 | ctx->arcset_chain_last->hdr_as = al; | |
1462 | ||
1463 | /* For any but "fail" chain-verify status, walk the entire chain in order by | |
1464 | instance. For fail, only the new arc-set. Accumulate the elements walked. */ | |
1465 | ||
7af11cd0 | 1466 | for (arc_set * as = Ustrcmp(status, US"fail") == 0 |
617d3932 JH |
1467 | ? ctx->arcset_chain_last : ctx->arcset_chain; |
1468 | as; as = as->next) | |
1469 | { | |
7af11cd0 | 1470 | arc_line * l; |
617d3932 JH |
1471 | /* Accumulate AAR then AMS then AS. Relaxed canonicalisation |
1472 | is required per standard. */ | |
1473 | ||
7af11cd0 JH |
1474 | badline_str = US"aar"; |
1475 | if (!(l = as->hdr_aar)) goto badline; | |
1476 | h = l->complete; | |
617d3932 | 1477 | hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); |
7af11cd0 JH |
1478 | badline_str = US"ams"; |
1479 | if (!(l = as->hdr_ams)) goto badline; | |
1480 | h = l->complete; | |
617d3932 | 1481 | hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); |
7af11cd0 JH |
1482 | badline_str = US"as"; |
1483 | if (!(l = as->hdr_as)) goto badline; | |
1484 | h = l->complete; | |
d9604f37 | 1485 | hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next)); |
617d3932 JH |
1486 | } |
1487 | ||
1488 | /* Calculate the signature from the accumulation */ | |
1489 | ||
1490 | if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AS")) | |
1491 | return NULL; | |
1492 | ||
1493 | /* Lose the trailing semicolon */ | |
1494 | arcset->ptr--; | |
1495 | arcset = arc_sign_append_sig(arcset, &sig); | |
1496 | DEBUG(D_transport) debug_printf("ARC: AS '%.*s'\n", arcset->ptr - 2, arcset->s); | |
1497 | ||
1498 | /* Finally, append the AMS and AAR to the new AS */ | |
1499 | ||
1500 | return string_catn(arcset, arcset_interim->s, arcset_interim->ptr); | |
7af11cd0 JH |
1501 | |
1502 | badline: | |
1503 | DEBUG(D_transport) | |
1504 | debug_printf("ARC: while building AS, missing %s in chain\n", badline_str); | |
1505 | return NULL; | |
617d3932 JH |
1506 | } |
1507 | ||
1508 | ||
1509 | /**************************************/ | |
1510 | ||
1511 | /* Return pointer to pdkim_bodyhash for given hash method, creating new | |
1512 | method if needed. | |
1513 | */ | |
1514 | ||
1515 | void * | |
1516 | arc_ams_setup_sign_bodyhash(void) | |
1517 | { | |
1518 | int canon_head, canon_body; | |
1519 | ||
1520 | DEBUG(D_transport) debug_printf("ARC: requesting bodyhash\n"); | |
1521 | pdkim_cstring_to_canons(US"relaxed", 7, &canon_head, &canon_body); /*XXX hardwired */ | |
1522 | return pdkim_set_bodyhash(&dkim_sign_ctx, | |
1523 | pdkim_hashname_to_hashtype(US"sha256", 6), /*XXX hardwired */ | |
1524 | canon_body, | |
1525 | -1); | |
1526 | } | |
1527 | ||
1528 | ||
1529 | ||
b3d9ebf5 JH |
1530 | void |
1531 | arc_sign_init(void) | |
1532 | { | |
1533 | memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx)); | |
1534 | } | |
1535 | ||
1536 | ||
1537 | ||
617d3932 JH |
1538 | /* A "normal" header line, identified by DKIM processing. These arrive before |
1539 | the call to arc_sign(), which carries any newly-created DKIM headers - and | |
1540 | those go textually before the normal ones in the message. | |
1541 | ||
1542 | We have to take the feed from DKIM as, in the transport-filter case, the | |
1543 | headers are not in memory at the time of the call to arc_sign(). | |
1544 | ||
1545 | Take a copy of the header and construct a reverse-order list. | |
1546 | Also parse ARC-chain headers and build the chain struct, retaining pointers | |
1547 | into the copies. | |
1548 | */ | |
1549 | ||
1550 | static const uschar * | |
1551 | arc_header_sign_feed(gstring * g) | |
1552 | { | |
1553 | uschar * s = string_copyn(g->s, g->ptr); | |
1554 | headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr); | |
1555 | return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE); | |
1556 | } | |
1557 | ||
1558 | ||
1559 | ||
1560 | /* ARC signing. Called from the smtp transport, if the arc_sign option is set. | |
1561 | The dkim_exim_sign() function has already been called, so will have hashed the | |
1562 | message body for us so long as we requested a hash previously. | |
1563 | ||
1564 | Arguments: | |
a93dbf44 JH |
1565 | signspec Three-element colon-sep list: identity, selector, privkey. |
1566 | Optional fourth element: comma-sep list of options. | |
617d3932 JH |
1567 | Already expanded |
1568 | sigheaders Any signature headers already generated, eg. by DKIM, or NULL | |
1569 | errstr Error string | |
1570 | ||
1571 | Return value | |
1572 | Set of headers to prepend to the message, including the supplied sigheaders | |
1573 | but not the plainheaders. | |
1574 | */ | |
1575 | ||
1576 | gstring * | |
1577 | arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr) | |
1578 | { | |
a93dbf44 JH |
1579 | const uschar * identity, * selector, * privkey, * opts, * s; |
1580 | unsigned options = 0; | |
617d3932 JH |
1581 | int sep = 0; |
1582 | header_line * headers; | |
1583 | hdr_rlist * rheaders; | |
1584 | blob ar; | |
1585 | int instance; | |
1586 | gstring * g = NULL; | |
1587 | pdkim_bodyhash * b; | |
1588 | ||
0e83c148 JH |
1589 | expire = now = 0; |
1590 | ||
617d3932 JH |
1591 | /* Parse the signing specification */ |
1592 | ||
1593 | identity = string_nextinlist(&signspec, &sep, NULL, 0); | |
1594 | selector = string_nextinlist(&signspec, &sep, NULL, 0); | |
69a82da3 | 1595 | if ( !*identity || !*selector |
617d3932 JH |
1596 | || !(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey) |
1597 | { | |
c3d43245 | 1598 | log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)", |
9f7d9fa1 | 1599 | !*identity ? "identity" : !*selector ? "selector" : "private-key"); |
c3d43245 | 1600 | return sigheaders ? sigheaders : string_get(0); |
617d3932 JH |
1601 | } |
1602 | if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey))) | |
c3d43245 | 1603 | return sigheaders ? sigheaders : string_get(0); |
617d3932 | 1604 | |
a93dbf44 JH |
1605 | if ((opts = string_nextinlist(&signspec, &sep, NULL, 0))) |
1606 | { | |
1607 | int osep = ','; | |
1608 | while ((s = string_nextinlist(&opts, &osep, NULL, 0))) | |
1609 | if (Ustrcmp(s, "timestamps") == 0) | |
0e83c148 | 1610 | { |
a93dbf44 | 1611 | options |= ARC_SIGN_OPT_TSTAMP; |
0e83c148 JH |
1612 | if (!now) now = time(NULL); |
1613 | } | |
1614 | else if (Ustrncmp(s, "expire", 6) == 0) | |
1615 | { | |
1616 | options |= ARC_SIGN_OPT_EXPIRE; | |
1617 | if (*(s += 6) == '=') | |
1618 | if (*++s == '+') | |
1619 | { | |
48224640 | 1620 | if (!(expire = (time_t)atoi(CS ++s))) |
0e83c148 JH |
1621 | expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA; |
1622 | if (!now) now = time(NULL); | |
1623 | expire += now; | |
1624 | } | |
1625 | else | |
48224640 | 1626 | expire = (time_t)atol(CS s); |
0e83c148 JH |
1627 | else |
1628 | { | |
1629 | if (!now) now = time(NULL); | |
1630 | expire = now + ARC_SIGN_DEFAULT_EXPIRE_DELTA; | |
1631 | } | |
1632 | } | |
a93dbf44 JH |
1633 | } |
1634 | ||
617d3932 JH |
1635 | DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity); |
1636 | ||
b3d9ebf5 JH |
1637 | /* Make an rlist of any new DKIM headers, then add the "normals" rlist to it. |
1638 | Then scan the list for an A-R header. */ | |
617d3932 JH |
1639 | |
1640 | string_from_gstring(sigheaders); | |
1641 | if ((rheaders = arc_sign_scan_headers(&arc_sign_ctx, sigheaders))) | |
1642 | { | |
1643 | hdr_rlist ** rp; | |
0c2250d1 JH |
1644 | for (rp = &headers_rlist; *rp; ) rp = &(*rp)->prev; |
1645 | *rp = rheaders; | |
617d3932 | 1646 | } |
b3d9ebf5 | 1647 | |
617d3932 JH |
1648 | /* Finally, build a normal-order headers list */ |
1649 | /*XXX only needed for hunt-the-AR? */ | |
0c2250d1 | 1650 | /*XXX also, we really should be accepting any number of ADMD-matching ARs */ |
b3d9ebf5 JH |
1651 | { |
1652 | header_line * hnext = NULL; | |
0c2250d1 JH |
1653 | for (rheaders = headers_rlist; rheaders; |
1654 | hnext = rheaders->h, rheaders = rheaders->prev) | |
b3d9ebf5 JH |
1655 | rheaders->h->next = hnext; |
1656 | headers = hnext; | |
1657 | } | |
617d3932 JH |
1658 | |
1659 | if (!(arc_sign_find_ar(headers, identity, &ar))) | |
1660 | { | |
c3d43245 | 1661 | log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing"); |
617d3932 JH |
1662 | return sigheaders ? sigheaders : string_get(0); |
1663 | } | |
1664 | ||
b3d9ebf5 JH |
1665 | /* We previously built the data-struct for the existing ARC chain, if any, using a headers |
1666 | feed from the DKIM module. Use that to give the instance number for the ARC set we are | |
1667 | about to build. */ | |
1668 | ||
1669 | DEBUG(D_transport) | |
1670 | if (arc_sign_ctx.arcset_chain_last) | |
1671 | debug_printf("ARC: existing chain highest instance: %d\n", | |
1672 | arc_sign_ctx.arcset_chain_last->instance); | |
1673 | else | |
1674 | debug_printf("ARC: no existing chain\n"); | |
1675 | ||
1676 | instance = arc_sign_ctx.arcset_chain_last ? arc_sign_ctx.arcset_chain_last->instance + 1 : 1; | |
1677 | ||
617d3932 JH |
1678 | /* |
1679 | - Generate AAR | |
1680 | - copy the A-R; prepend i= & identity | |
1681 | */ | |
1682 | ||
1683 | g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar); | |
1684 | ||
1685 | /* | |
1686 | - Generate AMS | |
1687 | - Looks fairly like a DKIM sig | |
1688 | - Cover all DKIM sig headers as well as the usuals | |
1689 | - ? oversigning? | |
1690 | - Covers the data | |
1691 | - we must have requested a suitable bodyhash previously | |
1692 | */ | |
1693 | ||
1694 | b = arc_ams_setup_sign_bodyhash(); | |
1695 | g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector, | |
a93dbf44 | 1696 | &b->bh, headers_rlist, privkey, options); |
617d3932 JH |
1697 | |
1698 | /* | |
1699 | - Generate AS | |
1700 | - no body coverage | |
1701 | - no h= tag; implicit coverage | |
1702 | - arc status from A-R | |
1703 | - if fail: | |
1704 | - coverage is just the new ARC set | |
1705 | including self (but with an empty b= in self) | |
1706 | - if non-fail: | |
1707 | - all ARC set headers, set-number order, aar then ams then as, | |
1708 | including self (but with an empty b= in self) | |
1709 | */ | |
1710 | ||
97e939df JH |
1711 | if (g) |
1712 | g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar, | |
a93dbf44 | 1713 | privkey, options); |
617d3932 JH |
1714 | |
1715 | /* Finally, append the dkim headers and return the lot. */ | |
1716 | ||
7b9822bf | 1717 | if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr); |
617d3932 | 1718 | (void) string_from_gstring(g); |
e59797e3 | 1719 | gstring_release_unused(g); |
617d3932 JH |
1720 | return g; |
1721 | } | |
1722 | ||
1723 | ||
1724 | /******************************************************************************/ | |
1725 | ||
1726 | /* Check to see if the line is an AMS and if so, set up to validate it. | |
1727 | Called from the DKIM input processing. This must be done now as the message | |
1728 | body data is hashed during input. | |
1729 | ||
1730 | We call the DKIM code to request a body-hash; it has the facility already | |
1731 | and the hash parameters might be common with other requests. | |
1732 | */ | |
1733 | ||
1734 | static const uschar * | |
1735 | arc_header_vfy_feed(gstring * g) | |
1736 | { | |
1737 | header_line h; | |
1738 | arc_line al; | |
1739 | pdkim_bodyhash * b; | |
1740 | uschar * errstr; | |
1741 | ||
1742 | if (!dkim_verify_ctx) return US"no dkim context"; | |
1743 | ||
1744 | if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS"; | |
1745 | ||
1746 | DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n"); | |
1747 | /* Parse the AMS header */ | |
1748 | ||
1749 | h.next = NULL; | |
1750 | h.slen = g->size; | |
1751 | h.text = g->s; | |
1752 | memset(&al, 0, sizeof(arc_line)); | |
1753 | if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE))) | |
1754 | { | |
1755 | DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr); | |
6f47da8d JH |
1756 | goto badline; |
1757 | } | |
1758 | ||
1759 | if (!al.a_hash.data) | |
1760 | { | |
1761 | DEBUG(D_acl) debug_printf("ARC: no a_hash from '%.*s'\n", h.slen, h.text); | |
1762 | goto badline; | |
617d3932 JH |
1763 | } |
1764 | ||
1765 | /* defaults */ | |
1766 | if (!al.c.data) | |
1767 | { | |
1768 | al.c_body.data = US"simple"; al.c_body.len = 6; | |
1769 | al.c_head = al.c_body; | |
1770 | } | |
1771 | ||
1772 | /* Ask the dkim code to calc a bodyhash with those specs */ | |
1773 | ||
1774 | if (!(b = arc_ams_setup_vfy_bodyhash(&al))) | |
1775 | return US"dkim hash setup fail"; | |
1776 | ||
1777 | /* Discard the reference; search again at verify time, knowing that one | |
1778 | should have been created here. */ | |
1779 | ||
1780 | return NULL; | |
6f47da8d JH |
1781 | |
1782 | badline: | |
1783 | return US"line parsing error"; | |
617d3932 JH |
1784 | } |
1785 | ||
1786 | ||
1787 | ||
1788 | /* A header line has been identified by DKIM processing. | |
1789 | ||
1790 | Arguments: | |
1791 | g Header line | |
1792 | is_vfy TRUE for verify mode or FALSE for signing mode | |
1793 | ||
1794 | Return: | |
1795 | NULL for success, or an error string (probably unused) | |
1796 | */ | |
1797 | ||
1798 | const uschar * | |
1799 | arc_header_feed(gstring * g, BOOL is_vfy) | |
1800 | { | |
1801 | return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(g); | |
1802 | } | |
1803 | ||
1804 | ||
1805 | ||
1806 | /******************************************************************************/ | |
1807 | ||
9cffa436 JH |
1808 | /* Construct the list of domains from the ARC chain after validation */ |
1809 | ||
1810 | uschar * | |
1811 | fn_arc_domains(void) | |
1812 | { | |
1813 | arc_set * as; | |
08bd2689 | 1814 | unsigned inst; |
9cffa436 JH |
1815 | gstring * g = NULL; |
1816 | ||
08bd2689 | 1817 | for (as = arc_verify_ctx.arcset_chain, inst = 1; as; as = as->next, inst++) |
9cffa436 | 1818 | { |
08bd2689 JH |
1819 | arc_line * hdr_as = as->hdr_as; |
1820 | if (hdr_as) | |
1821 | { | |
1822 | blob * d = &hdr_as->d; | |
1823 | ||
1824 | for (; inst < as->instance; inst++) | |
c8599aad | 1825 | g = string_catn(g, US":", 1); |
08bd2689 JH |
1826 | |
1827 | g = d->data && d->len | |
1828 | ? string_append_listele_n(g, ':', d->data, d->len) | |
c8599aad | 1829 | : string_catn(g, US":", 1); |
08bd2689 JH |
1830 | } |
1831 | else | |
c8599aad | 1832 | g = string_catn(g, US":", 1); |
9cffa436 JH |
1833 | } |
1834 | return g ? g->s : US""; | |
1835 | } | |
1836 | ||
1837 | ||
c8599aad | 1838 | /* Construct an Authentication-Results header portion, for the ARC module */ |
617d3932 JH |
1839 | |
1840 | gstring * | |
1841 | authres_arc(gstring * g) | |
1842 | { | |
1843 | if (arc_state) | |
1844 | { | |
1845 | arc_line * highest_ams; | |
978e78de | 1846 | int start = 0; /* Compiler quietening */ |
617d3932 JH |
1847 | DEBUG(D_acl) start = g->ptr; |
1848 | ||
1849 | g = string_append(g, 2, US";\n\tarc=", arc_state); | |
1850 | if (arc_received_instance > 0) | |
1851 | { | |
52f12a7c | 1852 | g = string_fmt_append(g, " (i=%d)", arc_received_instance); |
93c931f8 JH |
1853 | if (arc_state_reason) |
1854 | g = string_append(g, 3, US"(", arc_state_reason, US")"); | |
1855 | g = string_catn(g, US" header.s=", 10); | |
617d3932 JH |
1856 | highest_ams = arc_received->hdr_ams; |
1857 | g = string_catn(g, highest_ams->s.data, highest_ams->s.len); | |
1858 | ||
52f12a7c | 1859 | g = string_fmt_append(g, " arc.oldest-pass=%d", arc_oldest_pass); |
617d3932 JH |
1860 | |
1861 | if (sender_host_address) | |
99efa4cf | 1862 | g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address); |
617d3932 | 1863 | } |
b3d9ebf5 JH |
1864 | else if (arc_state_reason) |
1865 | g = string_append(g, 3, US" (", arc_state_reason, US")"); | |
617d3932 JH |
1866 | DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n", |
1867 | g->ptr - start - 3, g->s + start + 3); | |
1868 | } | |
1869 | else | |
1870 | DEBUG(D_acl) debug_printf("ARC: no authres\n"); | |
1871 | return g; | |
1872 | } | |
1873 | ||
1874 | ||
2b2cfa83 | 1875 | # endif /* DISABLE_DKIM */ |
617d3932 JH |
1876 | #endif /* EXPERIMENTAL_ARC */ |
1877 | /* vi: aw ai sw=2 | |
1878 | */ |