X-Git-Url: https://git.exim.org/users/jgh/exim.git/blobdiff_plain/ca9cb170c15a0c996549c256997b57c30d4b30dd..bd8fbe3606d80e5a3fc02fe71b521146c6938448:/src/src/pdkim/pdkim.c diff --git a/src/src/pdkim/pdkim.c b/src/src/pdkim/pdkim.c index 12423a71b..4309675e6 100644 --- a/src/src/pdkim/pdkim.c +++ b/src/src/pdkim/pdkim.c @@ -45,7 +45,7 @@ #include "rsa.h" #define PDKIM_SIGNATURE_VERSION "1" -#define PDKIM_PUB_RECORD_VERSION "DKIM1" +#define PDKIM_PUB_RECORD_VERSION US "DKIM1" #define PDKIM_MAX_HEADER_LEN 65536 #define PDKIM_MAX_HEADERS 512 @@ -62,51 +62,51 @@ /* -------------------------------------------------------------------------- */ struct pdkim_stringlist { - char *value; - int tag; - void *next; + uschar * value; + int tag; + void * next; }; /* -------------------------------------------------------------------------- */ /* A bunch of list constants */ -const char *pdkim_querymethods[] = { - "dns/txt", +const uschar * pdkim_querymethods[] = { + US"dns/txt", NULL }; -const char *pdkim_algos[] = { - "rsa-sha256", - "rsa-sha1", +const uschar * pdkim_algos[] = { + US"rsa-sha256", + US"rsa-sha1", NULL }; -const char *pdkim_canons[] = { - "simple", - "relaxed", +const uschar * pdkim_canons[] = { + US"simple", + US"relaxed", NULL }; -const char *pdkim_hashes[] = { - "sha256", - "sha1", +const uschar * pdkim_hashes[] = { + US"sha256", + US"sha1", NULL }; -const char *pdkim_keytypes[] = { - "rsa", +const uschar * pdkim_keytypes[] = { + US"rsa", NULL }; typedef struct pdkim_combined_canon_entry { - const char *str; + const uschar * str; int canon_headers; int canon_body; } pdkim_combined_canon_entry; pdkim_combined_canon_entry pdkim_combined_canons[] = { - { "simple/simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, - { "simple/relaxed", PDKIM_CANON_SIMPLE, PDKIM_CANON_RELAXED }, - { "relaxed/simple", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, - { "relaxed/relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_RELAXED }, - { "simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, - { "relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, - { NULL, 0, 0 } + { US"simple/simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, + { US"simple/relaxed", PDKIM_CANON_SIMPLE, PDKIM_CANON_RELAXED }, + { US"relaxed/simple", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, + { US"relaxed/relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_RELAXED }, + { US"simple", PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE }, + { US"relaxed", PDKIM_CANON_RELAXED, PDKIM_CANON_SIMPLE }, + { NULL, 0, 0 } }; @@ -115,26 +115,47 @@ pdkim_combined_canon_entry pdkim_combined_canons[] = { const char * pdkim_verify_status_str(int status) { - switch(status) { - case PDKIM_VERIFY_NONE: return "PDKIM_VERIFY_NONE"; - case PDKIM_VERIFY_INVALID: return "PDKIM_VERIFY_INVALID"; - case PDKIM_VERIFY_FAIL: return "PDKIM_VERIFY_FAIL"; - case PDKIM_VERIFY_PASS: return "PDKIM_VERIFY_PASS"; - default: return "PDKIM_VERIFY_UNKNOWN"; +switch(status) + { + case PDKIM_VERIFY_NONE: return "PDKIM_VERIFY_NONE"; + case PDKIM_VERIFY_INVALID: return "PDKIM_VERIFY_INVALID"; + case PDKIM_VERIFY_FAIL: return "PDKIM_VERIFY_FAIL"; + case PDKIM_VERIFY_PASS: return "PDKIM_VERIFY_PASS"; + default: return "PDKIM_VERIFY_UNKNOWN"; } } const char * pdkim_verify_ext_status_str(int ext_status) { - switch(ext_status) { - case PDKIM_VERIFY_FAIL_BODY: return "PDKIM_VERIFY_FAIL_BODY"; - case PDKIM_VERIFY_FAIL_MESSAGE: return "PDKIM_VERIFY_FAIL_MESSAGE"; - case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: return "PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE"; - case PDKIM_VERIFY_INVALID_BUFFER_SIZE: return "PDKIM_VERIFY_INVALID_BUFFER_SIZE"; - case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: return "PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD"; - case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return "PDKIM_VERIFY_INVALID_PUBKEY_IMPORT"; - default: return "PDKIM_VERIFY_UNKNOWN"; +switch(ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: return "PDKIM_VERIFY_FAIL_BODY"; + case PDKIM_VERIFY_FAIL_MESSAGE: return "PDKIM_VERIFY_FAIL_MESSAGE"; + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: return "PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE"; + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: return "PDKIM_VERIFY_INVALID_BUFFER_SIZE"; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: return "PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD"; + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return "PDKIM_VERIFY_INVALID_PUBKEY_IMPORT"; + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: return "PDKIM_VERIFY_INVALID_SIGNATURE_ERROR"; + case PDKIM_VERIFY_INVALID_DKIM_VERSION: return "PDKIM_VERIFY_INVALID_DKIM_VERSION"; + default: return "PDKIM_VERIFY_UNKNOWN"; + } +} + +const char * +pdkim_errstr(int status) +{ +switch(status) + { + case PDKIM_OK: return "OK"; + case PDKIM_FAIL: return "FAIL"; + case PDKIM_ERR_RSA_PRIVKEY: return "RSA_PRIVKEY"; + case PDKIM_ERR_RSA_SIGNING: return "RSA SIGNING"; + case PDKIM_ERR_LONG_LINE: return "RSA_LONG_LINE"; + case PDKIM_ERR_BUFFER_TOO_SMALL: return "BUFFER_TOO_SMALL"; + case PDKIM_SIGN_PRIVKEY_WRAP: return "PRIVKEY_WRAP"; + case PDKIM_SIGN_PRIVKEY_B64D: return "PRIVKEY_B64D"; + default: return "(unknown)"; } } @@ -224,7 +245,7 @@ pdkim_free_ctx(pdkim_ctx *ctx) /*XXX might be safer done using a pdkim_stringlist for "tick" */ static int -header_name_match(const uschar * header, char * tick) +header_name_match(const uschar * header, uschar * tick) { uschar * hname; uschar * lcopy; @@ -313,10 +334,10 @@ return relaxed; /* -------------------------------------------------------------------------- */ #define PDKIM_QP_ERROR_DECODE -1 -static char * -pdkim_decode_qp_char(char *qp_p, int *c) +static uschar * +pdkim_decode_qp_char(uschar *qp_p, int *c) { -char *initial_pos = qp_p; +uschar *initial_pos = qp_p; /* Advance one char */ qp_p++; @@ -338,7 +359,7 @@ return initial_pos; /* -------------------------------------------------------------------------- */ -static char * +static uschar * pdkim_decode_qp(uschar * str) { int nchar = 0; @@ -407,6 +428,10 @@ sig = store_get(sizeof(pdkim_signature)); memset(sig, 0, sizeof(pdkim_signature)); sig->bodylength = -1; +/* Set so invalid/missing data error display is accurate */ +sig->algo = -1; +sig->version = 0; + q = sig->rawsig_no_b_val = store_get(Ustrlen(raw_hdr)+1); for (p = raw_hdr; ; p++) @@ -436,7 +461,7 @@ for (p = raw_hdr; ; p++) if (where == PDKIM_HDR_TAG) { if (c >= 'a' && c <= 'z') - cur_tag = string_cat(cur_tag, &ts, &tl, p, 1); + cur_tag = string_catn(cur_tag, &ts, &tl, p, 1); if (c == '=') { @@ -476,8 +501,8 @@ for (p = raw_hdr; ; p++) case 'v': /* We only support version 1, and that is currently the only version there is. */ - if (Ustrcmp(cur_val, PDKIM_SIGNATURE_VERSION) == 0) - sig->version = 1; + sig->version = + Ustrcmp(cur_val, PDKIM_SIGNATURE_VERSION) == 0 ? 1 : -1; break; case 'a': for (i = 0; pdkim_algos[i]; i++) @@ -531,7 +556,7 @@ for (p = raw_hdr; ; p++) where = PDKIM_HDR_LIMBO; } else - cur_val = string_cat(cur_val, &vs, &vl, p, 1); + cur_val = string_catn(cur_val, &vs, &vl, p, 1); } NEXT_CHAR: @@ -542,10 +567,6 @@ NEXT_CHAR: *q++ = c; } -/* Make sure the most important bits are there. */ -if (!sig->version) - return NULL; - *q = '\0'; /* Chomp raw header. The final newline must not be added to the signature. */ while (--q > sig->rawsig_no_b_val && (*q == '\r' || *q == '\n')) @@ -555,14 +576,14 @@ DEBUG(D_acl) { debug_printf( "PDKIM >> Raw signature w/o b= tag value >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); - pdkim_quoteprint(US sig->rawsig_no_b_val, strlen(sig->rawsig_no_b_val)); + pdkim_quoteprint(US sig->rawsig_no_b_val, Ustrlen(sig->rawsig_no_b_val)); debug_printf( "PDKIM >> Sig size: %4u bits\n", (unsigned) sig->sigdata.len*8); debug_printf( "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); } -exim_sha_init(&sig->body_hash, sig->algo == PDKIM_ALGO_RSA_SHA1); +exim_sha_init(&sig->body_hash, sig->algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256); return sig; } @@ -570,10 +591,10 @@ return sig; /* -------------------------------------------------------------------------- */ static pdkim_pubkey * -pdkim_parse_pubkey_record(pdkim_ctx *ctx, char *raw_record) +pdkim_parse_pubkey_record(pdkim_ctx *ctx, const uschar *raw_record) { pdkim_pubkey *pub; -char *p; +const uschar *p; uschar * cur_tag = NULL; int ts = 0, tl = 0; uschar * cur_val = NULL; int vs = 0, vl = 0; int where = PDKIM_HDR_LIMBO; @@ -582,90 +603,87 @@ pub = store_get(sizeof(pdkim_pubkey)); memset(pub, 0, sizeof(pdkim_pubkey)); for (p = raw_record; ; p++) - { - char c = *p; - - /* Ignore FWS */ - if (c == '\r' || c == '\n') - goto NEXT_CHAR; - - if (where == PDKIM_HDR_LIMBO) - { - /* In limbo, just wait for a tag-char to appear */ - if (!(c >= 'a' && c <= 'z')) - goto NEXT_CHAR; - - where = PDKIM_HDR_TAG; - } - - if (where == PDKIM_HDR_TAG) { - if (c >= 'a' && c <= 'z') - cur_tag = string_cat(cur_tag, &ts, &tl, p, 1); + uschar c = *p; - if (c == '=') + /* Ignore FWS */ + if (c != '\r' && c != '\n') switch (where) { - cur_tag[tl] = '\0'; - where = PDKIM_HDR_VALUE; - goto NEXT_CHAR; - } - } + case PDKIM_HDR_LIMBO: /* In limbo, just wait for a tag-char to appear */ + if (!(c >= 'a' && c <= 'z')) + break; + where = PDKIM_HDR_TAG; + /*FALLTHROUGH*/ - if (where == PDKIM_HDR_VALUE) - { - if (c == ';' || c == '\0') - { - if (tl && vl) - { - cur_val[vl] = '\0'; - pdkim_strtrim(cur_val); - DEBUG(D_acl) debug_printf(" %s=%s\n", cur_tag, cur_val); + case PDKIM_HDR_TAG: + if (c >= 'a' && c <= 'z') + cur_tag = string_catn(cur_tag, &ts, &tl, p, 1); - switch (cur_tag[0]) + if (c == '=') { - case 'v': - /* This tag isn't evaluated because: - - We only support version DKIM1. - - Which is the default for this value (set below) - - Other versions are currently not specified. */ - break; - case 'h': - case 'k': - pub->hashes = string_copy(cur_val); break; - case 'g': - pub->granularity = string_copy(cur_val); break; - case 'n': - pub->notes = pdkim_decode_qp(cur_val); break; - case 'p': - pdkim_decode_base64(US cur_val, &pub->key); - break; - case 's': - pub->srvtype = string_copy(cur_val); break; - case 't': - if (strchr(cur_val, 'y') != NULL) pub->testing = 1; - if (strchr(cur_val, 's') != NULL) pub->no_subdomaining = 1; - break; - default: - DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); - break; + cur_tag[tl] = '\0'; + where = PDKIM_HDR_VALUE; } - } - tl = 0; - vl = 0; - where = PDKIM_HDR_LIMBO; + break; + + case PDKIM_HDR_VALUE: + if (c == ';' || c == '\0') + { + if (tl && vl) + { + cur_val[vl] = '\0'; + pdkim_strtrim(cur_val); + DEBUG(D_acl) debug_printf(" %s=%s\n", cur_tag, cur_val); + + switch (cur_tag[0]) + { + case 'v': + pub->version = string_copy(cur_val); break; + case 'h': + case 'k': +/* This field appears to never be used. Also, unclear why +a 'k' (key-type_ would go in this field name. There is a field +"keytype", also never used. + pub->hashes = string_copy(cur_val); +*/ + break; + case 'g': + pub->granularity = string_copy(cur_val); break; + case 'n': + pub->notes = pdkim_decode_qp(cur_val); break; + case 'p': + pdkim_decode_base64(US cur_val, &pub->key); break; + case 's': + pub->srvtype = string_copy(cur_val); break; + case 't': + if (Ustrchr(cur_val, 'y') != NULL) pub->testing = 1; + if (Ustrchr(cur_val, 's') != NULL) pub->no_subdomaining = 1; + break; + default: + DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); + break; + } + } + tl = 0; + vl = 0; + where = PDKIM_HDR_LIMBO; + } + else + cur_val = string_catn(cur_val, &vs, &vl, p, 1); + break; } - else - cur_val = string_cat(cur_val, &vs, &vl, p, 1); - } -NEXT_CHAR: - if (c == '\0') break; - } + if (c == '\0') break; + } /* Set fallback defaults */ if (!pub->version ) pub->version = string_copy(PDKIM_PUB_RECORD_VERSION); +else if (Ustrcmp(pub->version, PDKIM_PUB_RECORD_VERSION) != 0) return NULL; + if (!pub->granularity) pub->granularity = string_copy(US"*"); -if (!pub->keytype ) pub->keytype = string_copy("rsa"); +/* +if (!pub->keytype ) pub->keytype = string_copy(US"rsa"); +*/ if (!pub->srvtype ) pub->srvtype = string_copy(US"*"); /* p= is required */ @@ -702,7 +720,12 @@ while (sig) const char *p; int q = 0; - relaxed_data = store_get(len+1); + /* We want to be able to free this else we allocate + for the entire message which could be many MB. Since + we don't know what allocations the SHA routines might + do, not safe to use store_get()/store_reset(). */ + + relaxed_data = store_malloc(len+1); for (p = data; *p; p++) { @@ -738,7 +761,7 @@ while (sig) if (canon_len > 0) { - exim_sha_update(&sig->body_hash, CCS canon_data, canon_len); + exim_sha_update(&sig->body_hash, CUS canon_data, canon_len); sig->signed_body_bytes += canon_len; DEBUG(D_acl) pdkim_quoteprint(canon_data, canon_len); } @@ -746,6 +769,7 @@ while (sig) sig = sig->next; } +if (relaxed_data) store_free(relaxed_data); return PDKIM_OK; } @@ -767,13 +791,13 @@ for (sig = ctx->sig; sig; sig = sig->next) DEBUG(D_acl) { debug_printf("PDKIM [%s] Body bytes hashed: %lu\n" - "PDKIM [%s] bh computed: ", + "PDKIM [%s] Body hash computed: ", sig->domain, sig->signed_body_bytes, sig->domain); pdkim_hexprint(CUS bh.data, bh.len); } /* SIGNING -------------------------------------------------------------- */ - if (ctx->mode == PDKIM_MODE_SIGN) + if (ctx->flags & PDKIM_MODE_SIGN) { sig->bodyhash = bh; @@ -795,7 +819,7 @@ for (sig = ctx->sig; sig; sig = sig->next) { DEBUG(D_acl) { - debug_printf("PDKIM [%s] bh signature: ", sig->domain); + debug_printf("PDKIM [%s] Body hash signature from headers: ", sig->domain); pdkim_hexprint(sig->bodyhash.data, exim_sha_hashlen(&sig->body_hash)); debug_printf("PDKIM [%s] Body hash did NOT verify\n", sig->domain); @@ -809,8 +833,32 @@ for (sig = ctx->sig; sig; sig = sig->next) +static int +pdkim_body_complete(pdkim_ctx * ctx) +{ +pdkim_signature * sig = ctx->sig; /*XXX assumes only one sig */ + +/* In simple body mode, if any empty lines were buffered, +replace with one. rfc 4871 3.4.3 */ +/*XXX checking the signed-body-bytes is a gross hack; I think +it indicates that all linebreaks should be buffered, including +the one terminating a text line */ + +if ( sig && sig->canon_body == PDKIM_CANON_SIMPLE + && sig->signed_body_bytes == 0 + && ctx->num_buffered_crlf > 0 + ) + pdkim_update_bodyhash(ctx, "\r\n", 2); + +ctx->flags |= PDKIM_SEEN_EOD; +ctx->linebuf_offset = 0; +return PDKIM_OK; +} + + + /* -------------------------------------------------------------------------- */ -/* Callback from pdkim_feed below for processing complete body lines */ +/* Call from pdkim_feed below for processing complete body lines */ static int pdkim_bodyline_complete(pdkim_ctx *ctx) @@ -820,33 +868,23 @@ int n = ctx->linebuf_offset; pdkim_signature *sig = ctx->sig; /*XXX assumes only one sig */ /* Ignore extra data if we've seen the end-of-data marker */ -if (ctx->seen_eod) goto BAIL; +if (ctx->flags & PDKIM_SEEN_EOD) goto BAIL; /* We've always got one extra byte to stuff a zero ... */ ctx->linebuf[ctx->linebuf_offset] = '\0'; /* Terminate on EOD marker */ -if (memcmp(p, ".\r\n", 3) == 0) +if (ctx->flags & PDKIM_DOT_TERM) { - /* In simple body mode, if any empty lines were buffered, - replace with one. rfc 4871 3.4.3 */ - /*XXX checking the signed-body-bytes is a gross hack; I think - it indicates that all linebreaks should be buffered, including - the one terminating a text line */ - if ( sig && sig->canon_body == PDKIM_CANON_SIMPLE - && sig->signed_body_bytes == 0 - && ctx->num_buffered_crlf > 0 - ) - pdkim_update_bodyhash(ctx, "\r\n", 2); + if (memcmp(p, ".\r\n", 3) == 0) + return pdkim_body_complete(ctx); - ctx->seen_eod = TRUE; - goto BAIL; - } -/* Unstuff dots */ -if (memcmp(p, "..", 2) == 0) - { - p++; - n--; + /* Unstuff dots */ + if (memcmp(p, "..", 2) == 0) + { + p++; + n--; + } } /* Empty lines need to be buffered until we find a non-empty line */ @@ -906,7 +944,7 @@ ctx->num_headers++; if (ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL; /* SIGNING -------------------------------------------------------------- */ -if (ctx->mode == PDKIM_MODE_SIGN) +if (ctx->flags & PDKIM_MODE_SIGN) { pdkim_signature *sig; @@ -919,11 +957,16 @@ if (ctx->mode == PDKIM_MODE_SIGN) /* VERIFICATION ----------------------------------------------------------- */ /* DKIM-Signature: headers are added to the verification list */ -if (ctx->mode == PDKIM_MODE_VERIFY) +else { - if (strncasecmp(ctx->cur_header, + DEBUG(D_acl) + { + debug_printf("PDKIM >> raw hdr: "); + pdkim_quoteprint(CUS ctx->cur_header, Ustrlen(ctx->cur_header)); + } + if (strncasecmp(CCS ctx->cur_header, DKIM_SIGNATURE_HEADERNAME, - strlen(DKIM_SIGNATURE_HEADERNAME)) == 0) + Ustrlen(DKIM_SIGNATURE_HEADERNAME)) == 0) { pdkim_signature *new_sig; @@ -969,57 +1012,75 @@ pdkim_feed(pdkim_ctx *ctx, char *data, int len) { int p; -for (p = 0; ppast_headers) + if (ctx->flags & PDKIM_PAST_HDRS) { + if (c == '\n' && !(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ + { + ctx->linebuf[ctx->linebuf_offset++] = '\r'; + if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) + return PDKIM_ERR_LONG_LINE; + } + /* Processing body byte */ ctx->linebuf[ctx->linebuf_offset++] = c; - if (c == '\n') + if (c == '\r') + ctx->flags |= PDKIM_SEEN_CR; + else if (c == '\n') { - int rc = pdkim_bodyline_complete(ctx); /* End of line */ - if (rc != PDKIM_OK) return rc; + int rc; + ctx->flags &= ~PDKIM_SEEN_CR; + if ((rc = pdkim_bodyline_complete(ctx)) != PDKIM_OK) + return rc; } - if (ctx->linebuf_offset == (PDKIM_MAX_BODY_LINE_LEN-1)) + + if (ctx->linebuf_offset == PDKIM_MAX_BODY_LINE_LEN-1) return PDKIM_ERR_LONG_LINE; } else { /* Processing header byte */ - if (c != '\r') + if (c == '\r') + ctx->flags |= PDKIM_SEEN_CR; + else if (c == '\n') { - if (c == '\n') - { - if (ctx->seen_lf) - { - int rc = pdkim_header_complete(ctx); /* Seen last header line */ - if (rc != PDKIM_OK) return rc; - - ctx->past_headers = TRUE; - ctx->seen_lf = 0; - DEBUG(D_acl) debug_printf( - "PDKIM >> Body data for hash, canonicalized >>>>>>>>>>>>>>>>>>>>>>\n"); - continue; - } - else - ctx->seen_lf = TRUE; + if (!(ctx->flags & PDKIM_SEEN_CR)) /* emulate the CR */ + ctx->cur_header = string_catn(ctx->cur_header, &ctx->cur_header_size, + &ctx->cur_header_len, CUS "\r", 1); + + if (ctx->flags & PDKIM_SEEN_LF) + { + int rc = pdkim_header_complete(ctx); /* Seen last header line */ + if (rc != PDKIM_OK) return rc; + + ctx->flags = ctx->flags & ~(PDKIM_SEEN_LF|PDKIM_SEEN_CR) | PDKIM_PAST_HDRS; + DEBUG(D_acl) debug_printf( + "PDKIM >> Body data for hash, canonicalized >>>>>>>>>>>>>>>>>>>>>>\n"); + continue; } - else if (ctx->seen_lf) - { - if (!(c == '\t' || c == ' ')) - { - int rc = pdkim_header_complete(ctx); /* End of header */ - if (rc != PDKIM_OK) return rc; - } - ctx->seen_lf = FALSE; + else + ctx->flags = ctx->flags & ~PDKIM_SEEN_CR | PDKIM_SEEN_LF; + } + else if (ctx->flags & PDKIM_SEEN_LF) + { + if (!(c == '\t' || c == ' ')) + { + int rc = pdkim_header_complete(ctx); /* End of header */ + if (rc != PDKIM_OK) return rc; } + ctx->flags &= ~PDKIM_SEEN_LF; } if (ctx->cur_header_len < PDKIM_MAX_HEADER_LEN) - ctx->cur_header = string_cat(ctx->cur_header, &ctx->cur_header_size, - &ctx->cur_header_len, &data[p], 1); + ctx->cur_header = string_catn(ctx->cur_header, &ctx->cur_header_size, + &ctx->cur_header_len, CUS &data[p], 1); } } return PDKIM_OK; @@ -1032,7 +1093,7 @@ static uschar * pdkim_hdr_cont(uschar * str, int * size, int * ptr, int * col) { *col = 1; -return string_cat(str, size, ptr, US"\r\n\t", 3); +return string_catn(str, size, ptr, US"\r\n\t", 3); } @@ -1072,7 +1133,7 @@ if (pad) l = Ustrlen(pad); if (*col + l > 78) str = pdkim_hdr_cont(str, size, ptr, col); - str = string_cat(str, size, ptr, pad, l); + str = string_catn(str, size, ptr, pad, l); *col += l; } @@ -1090,7 +1151,7 @@ while (l>77) { /* this fragment will not fit on a single line */ if (pad) { - str = string_cat(str, size, ptr, US" ", 1); + str = string_catn(str, size, ptr, US" ", 1); *col += 1; pad = NULL; /* only want this once */ l--; @@ -1100,7 +1161,7 @@ while (l>77) { size_t sl = Ustrlen(intro); - str = string_cat(str, size, ptr, intro, sl); + str = string_catn(str, size, ptr, intro, sl); *col += sl; l -= sl; intro = NULL; /* only want this once */ @@ -1111,7 +1172,7 @@ while (l>77) size_t sl = Ustrlen(payload); size_t chomp = *col+sl < 77 ? sl : 78-*col; - str = string_cat(str, size, ptr, payload, chomp); + str = string_catn(str, size, ptr, payload, chomp); *col += chomp; payload += chomp; l -= chomp-1; @@ -1129,7 +1190,7 @@ if (*col + l > 78) if (pad) { - str = string_cat(str, size, ptr, US" ", 1); + str = string_catn(str, size, ptr, US" ", 1); *col += 1; pad = NULL; } @@ -1138,7 +1199,7 @@ if (intro) { size_t sl = Ustrlen(intro); - str = string_cat(str, size, ptr, intro, sl); + str = string_catn(str, size, ptr, intro, sl); *col += sl; l -= sl; intro = NULL; @@ -1148,7 +1209,7 @@ if (payload) { size_t sl = Ustrlen(payload); - str = string_cat(str, size, ptr, payload, sl); + str = string_catn(str, size, ptr, payload, sl); *col += sl; } @@ -1167,15 +1228,15 @@ int col = 0; uschar * hdr; int hdr_size = 0, hdr_len = 0; uschar * canon_all; int can_size = 0, can_len = 0; -canon_all = string_cat(NULL, &can_size, &can_len, - pdkim_canons[sig->canon_headers], -1); -canon_all = string_cat(canon_all, &can_size, &can_len, US"/", 1); -canon_all = string_cat(canon_all, &can_size, &can_len, - pdkim_canons[sig->canon_body], -1); +canon_all = string_cat (NULL, &can_size, &can_len, + pdkim_canons[sig->canon_headers]); +canon_all = string_catn(canon_all, &can_size, &can_len, US"/", 1); +canon_all = string_cat (canon_all, &can_size, &can_len, + pdkim_canons[sig->canon_body]); canon_all[can_len] = '\0'; hdr = string_cat(NULL, &hdr_size, &hdr_len, - "DKIM-Signature: v="PDKIM_SIGNATURE_VERSION, -1); + US"DKIM-Signature: v="PDKIM_SIGNATURE_VERSION); col = hdr_len; /* Required and static bits */ @@ -1192,7 +1253,7 @@ hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"s=", /* list of header names can be split between items. */ { - uschar * n = CS string_copy(sig->headernames); + uschar * n = string_copy(sig->headernames); uschar * i = US"h="; uschar * s = US";"; @@ -1225,25 +1286,25 @@ if (sig->identity) if (sig->created > 0) { - char minibuf[20]; + uschar minibuf[20]; - snprintf(minibuf, 20, "%lu", sig->created); + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->created); hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"t=", minibuf); } if (sig->expires > 0) { - char minibuf[20]; + uschar minibuf[20]; - snprintf(minibuf, 20, "%lu", sig->expires); + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->expires); hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"x=", minibuf); } if (sig->bodylength >= 0) { - char minibuf[20]; + uschar minibuf[20]; - snprintf(minibuf, 20, "%lu", sig->bodylength); + snprintf(CS minibuf, sizeof(minibuf), "%lu", sig->bodylength); hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"l=", minibuf); } @@ -1259,14 +1320,80 @@ return hdr; } +/* -------------------------------------------------------------------------- */ + +static pdkim_pubkey * +pdkim_key_from_dns(pdkim_ctx * ctx, pdkim_signature * sig, ev_ctx * vctx) +{ +uschar * dns_txt_name, * dns_txt_reply; +pdkim_pubkey * p; +const uschar * errstr; + +/* Fetch public key for signing domain, from DNS */ + +dns_txt_name = string_sprintf("%s._domainkey.%s.", sig->selector, sig->domain); + +dns_txt_reply = store_get(PDKIM_DNS_TXT_MAX_RECLEN); +memset(dns_txt_reply, 0, PDKIM_DNS_TXT_MAX_RECLEN); + +if ( ctx->dns_txt_callback(CS dns_txt_name, CS dns_txt_reply) != PDKIM_OK + || dns_txt_reply[0] == '\0' + ) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE; + return NULL; + } + +DEBUG(D_acl) + { + debug_printf( + "PDKIM >> Parsing public key record >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + " Raw record: "); + pdkim_quoteprint(CUS dns_txt_reply, Ustrlen(dns_txt_reply)); + } + +if ( !(p = pdkim_parse_pubkey_record(ctx, CUS dns_txt_reply)) + || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) + ) + { + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD; + + DEBUG(D_acl) + { + if (p) + debug_printf(" Invalid public key service type '%s'\n", p->srvtype); + else + debug_printf(" Error while parsing public key record\n"); + debug_printf( + "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + return NULL; + } + +DEBUG(D_acl) debug_printf( + "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + +/* Import public key */ +if ((errstr = exim_rsa_verify_init(&p->key, vctx))) + { + DEBUG(D_acl) debug_printf("verify_init: %s\n", errstr); + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; + return NULL; + } + +return p; +} + + /* -------------------------------------------------------------------------- */ DLLEXPORT int pdkim_feed_finish(pdkim_ctx *ctx, pdkim_signature **return_signatures) { pdkim_signature *sig = ctx->sig; -uschar * headernames = NULL; /* Collected signed header names */ -int hs = 0, hl = 0; /* Check if we must still flush a (partial) header. If that is the case, the message has no body, and we must compute a body hash @@ -1288,7 +1415,7 @@ while (sig) { BOOL is_sha1 = sig->algo == PDKIM_ALGO_RSA_SHA1; hctx hhash_ctx; - uschar * sig_hdr; + uschar * sig_hdr = US""; blob hhash; blob hdata; int hdata_alloc = 0; @@ -1296,10 +1423,10 @@ while (sig) hdata.data = NULL; hdata.len = 0; - exim_sha_init(&hhash_ctx, is_sha1); + exim_sha_init(&hhash_ctx, is_sha1 ? HASH_SHA1 : HASH_SHA256); DEBUG(D_acl) debug_printf( - "PDKIM >> Hashed header data, canonicalized, in sequence >>>>>>>>>>>>>>\n"); + "PDKIM >> Header data for hash, canonicalized, in sequence >>>>>>>>>>>>>>\n"); /* SIGNING ---------------------------------------------------------------- */ /* When signing, walk through our header list and add them to the hash. As we @@ -1307,8 +1434,10 @@ while (sig) Then append to that list any remaining header names for which there was no header to sign. */ - if (ctx->mode == PDKIM_MODE_SIGN) + if (ctx->flags & PDKIM_MODE_SIGN) { + uschar * headernames = NULL; /* Collected signed header names */ + int hs = 0, hl = 0; pdkim_stringlist *p; const uschar * l; uschar * s; @@ -1321,7 +1450,7 @@ while (sig) /* Collect header names (Note: colon presence is guaranteed here) */ uschar * q = Ustrchr(p->value, ':'); - headernames = string_cat(headernames, &hs, &hl, + headernames = string_catn(headernames, &hs, &hl, p->value, (q - US p->value) + (p->next ? 1 : 0)); rh = sig->canon_headers == PDKIM_CANON_RELAXED @@ -1329,7 +1458,7 @@ while (sig) : string_copy(CUS p->value); /* just copy it for simple canon */ /* Feed header to the hash algorithm */ - exim_sha_update(&hhash_ctx, CCS rh, Ustrlen(rh)); + exim_sha_update(&hhash_ctx, CUS rh, Ustrlen(rh)); /* Remember headers block for signing (when the library cannot do incremental) */ (void) exim_rsa_data_append(&hdata, &hdata_alloc, rh); @@ -1342,15 +1471,14 @@ while (sig) if (*s != '_') { /*SSS string_append_listele() */ if (hl > 0 && headernames[hl-1] != ':') - headernames = string_cat(headernames, &hs, &hl, US":", 1); + headernames = string_catn(headernames, &hs, &hl, US":", 1); - headernames = string_cat(headernames, &hs, &hl, s, -1); + headernames = string_cat(headernames, &hs, &hl, s); } headernames[hl] = '\0'; /* Copy headernames to signature struct */ sig->headernames = headernames; - headernames = NULL, hs = hl = 0; /* Create signature header with b= omitted */ sig_hdr = pdkim_create_header(sig, FALSE); @@ -1361,44 +1489,49 @@ while (sig) add the headers to the hash in that order. */ else { - uschar * b = string_copy(sig->headernames); - uschar * p = b; + uschar * p = sig->headernames; uschar * q; pdkim_stringlist * hdrs; - /* clear tags */ - for (hdrs = ctx->headers; hdrs; hdrs = hdrs->next) - hdrs->tag = 0; - - while(1) + if (p) { - if ((q = Ustrchr(p, ':'))) - *q = '\0'; - -/*XXX walk the list of headers in same order as received. */ + /* clear tags */ for (hdrs = ctx->headers; hdrs; hdrs = hdrs->next) - if ( hdrs->tag == 0 - && strncasecmp(hdrs->value, CS p, Ustrlen(p)) == 0 - && (hdrs->value)[Ustrlen(p)] == ':' - ) - { - uschar * rh = sig->canon_headers == PDKIM_CANON_RELAXED - ? pdkim_relax_header(hdrs->value, 1) /* cook header for relaxed canon */ - : string_copy(CUS hdrs->value); /* just copy it for simple canon */ + hdrs->tag = 0; - /* Feed header to the hash algorithm */ - exim_sha_update(&hhash_ctx, CCS rh, Ustrlen(rh)); + p = string_copy(p); + while(1) + { + if ((q = Ustrchr(p, ':'))) + *q = '\0'; + + /*XXX walk the list of headers in same order as received. */ + for (hdrs = ctx->headers; hdrs; hdrs = hdrs->next) + if ( hdrs->tag == 0 + && strncasecmp(CCS hdrs->value, CCS p, Ustrlen(p)) == 0 + && (hdrs->value)[Ustrlen(p)] == ':' + ) + { + /* cook header for relaxed canon, or just copy it for simple */ + + uschar * rh = sig->canon_headers == PDKIM_CANON_RELAXED + ? pdkim_relax_header(hdrs->value, 1) + : string_copy(CUS hdrs->value); + + /* Feed header to the hash algorithm */ + exim_sha_update(&hhash_ctx, CUS rh, Ustrlen(rh)); + + DEBUG(D_acl) pdkim_quoteprint(rh, Ustrlen(rh)); + hdrs->tag = 1; + break; + } - DEBUG(D_acl) pdkim_quoteprint(rh, Ustrlen(rh)); - hdrs->tag = 1; - break; - } + if (!q) break; + p = q+1; + } - if (!q) break; - p = q+1; + sig_hdr = string_copy(sig->rawsig_no_b_val); } - - sig_hdr = string_copy(sig->rawsig_no_b_val); } DEBUG(D_acl) debug_printf( @@ -1412,27 +1545,27 @@ while (sig) { debug_printf( "PDKIM >> Signed DKIM-Signature header, canonicalized >>>>>>>>>>>>>>>>>\n"); - pdkim_quoteprint(CUS sig_hdr, strlen(sig_hdr)); + pdkim_quoteprint(CUS sig_hdr, Ustrlen(sig_hdr)); debug_printf( "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); } /* Finalize header hash */ - exim_sha_update(&hhash_ctx, sig_hdr, strlen(sig_hdr)); + exim_sha_update(&hhash_ctx, CUS sig_hdr, Ustrlen(sig_hdr)); exim_sha_finish(&hhash_ctx, &hhash); DEBUG(D_acl) { - debug_printf("PDKIM [%s] hh computed: ", sig->domain); + debug_printf("PDKIM [%s] Header hash computed: ", sig->domain); pdkim_hexprint(hhash.data, hhash.len); } /* Remember headers block for signing (when the library cannot do incremental) */ - if (ctx->mode == PDKIM_MODE_SIGN) + if (ctx->flags & PDKIM_MODE_SIGN) (void) exim_rsa_data_append(&hdata, &hdata_alloc, US sig_hdr); /* SIGNING ---------------------------------------------------------------- */ - if (ctx->mode == PDKIM_MODE_SIGN) + if (ctx->flags & PDKIM_MODE_SIGN) { es_ctx sctx; const uschar * errstr; @@ -1472,55 +1605,41 @@ while (sig) { ev_ctx vctx; const uschar * errstr; - - uschar *dns_txt_name, *dns_txt_reply; - - /* Fetch public key for signing domain, from DNS */ - - dns_txt_name = string_sprintf("%s._domainkey.%s.", - sig->selector, sig->domain); - - dns_txt_reply = store_get(PDKIM_DNS_TXT_MAX_RECLEN); - memset(dns_txt_reply, 0, PDKIM_DNS_TXT_MAX_RECLEN); - - if ( ctx->dns_txt_callback(CS dns_txt_name, CS dns_txt_reply) != PDKIM_OK - || dns_txt_reply[0] == '\0') + pdkim_pubkey * p; + + /* Make sure we have all required signature tags */ + if (!( sig->domain && *sig->domain + && sig->selector && *sig->selector + && sig->headernames && *sig->headernames + && sig->bodyhash.data + && sig->sigdata.data + && sig->algo > -1 + && sig->version + ) ) { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE; - goto NEXT_VERIFY; - } + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_SIGNATURE_ERROR; - DEBUG(D_acl) - { - debug_printf( - "PDKIM >> Parsing public key record >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" - " Raw record: "); - pdkim_quoteprint(CUS dns_txt_reply, Ustrlen(dns_txt_reply)); + DEBUG(D_acl) debug_printf( + " Error in DKIM-Signature header: tags missing or invalid\n" + "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + goto NEXT_VERIFY; } - if (!(sig->pubkey = pdkim_parse_pubkey_record(ctx, dns_txt_reply))) + /* Make sure sig uses supported DKIM version (only v1) */ + if (sig->version != 1) { - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD; + sig->verify_status = PDKIM_VERIFY_INVALID; + sig->verify_ext_status = PDKIM_VERIFY_INVALID_DKIM_VERSION; DEBUG(D_acl) debug_printf( - " Error while parsing public key record\n" - "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + " Error in DKIM-Signature header: unsupported DKIM version\n" + "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); goto NEXT_VERIFY; } - DEBUG(D_acl) debug_printf( - "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); - - /* Import public key */ - if ((errstr = exim_rsa_verify_init(&sig->pubkey->key, &vctx))) - { - DEBUG(D_acl) debug_printf("verify_init: %s\n", errstr); - sig->verify_status = PDKIM_VERIFY_INVALID; - sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_IMPORT; + if (!(sig->pubkey = pdkim_key_from_dns(ctx, sig, &vctx))) goto NEXT_VERIFY; - } /* Check the signature */ if ((errstr = exim_rsa_verify(&vctx, is_sha1, &hhash, &sig->sigdata))) @@ -1532,7 +1651,7 @@ while (sig) } - /* We have a winner! (if bodydhash was correct earlier) */ + /* We have a winner! (if bodyhash was correct earlier) */ if (sig->verify_status == PDKIM_VERIFY_NONE) sig->verify_status = PDKIM_VERIFY_PASS; @@ -1564,15 +1683,15 @@ return PDKIM_OK; /* -------------------------------------------------------------------------- */ DLLEXPORT pdkim_ctx * -pdkim_init_verify(int(*dns_txt_callback)(char *, char *)) +pdkim_init_verify(int(*dns_txt_callback)(char *, char *), BOOL dot_stuffing) { pdkim_ctx * ctx; ctx = store_get(sizeof(pdkim_ctx)); memset(ctx, 0, sizeof(pdkim_ctx)); +if (dot_stuffing) ctx->flags = PDKIM_DOT_TERM; ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN); -ctx->mode = PDKIM_MODE_VERIFY; ctx->dns_txt_callback = dns_txt_callback; return ctx; @@ -1582,33 +1701,47 @@ return ctx; /* -------------------------------------------------------------------------- */ DLLEXPORT pdkim_ctx * -pdkim_init_sign(char *domain, char *selector, char *rsa_privkey, int algo) +pdkim_init_sign(char * domain, char * selector, char * rsa_privkey, int algo, + BOOL dot_stuffed, int(*dns_txt_callback)(char *, char *)) { -pdkim_ctx *ctx; -pdkim_signature *sig; +pdkim_ctx * ctx; +pdkim_signature * sig; if (!domain || !selector || !rsa_privkey) return NULL; -ctx = store_get(sizeof(pdkim_ctx)); +ctx = store_get(sizeof(pdkim_ctx) + PDKIM_MAX_BODY_LINE_LEN + sizeof(pdkim_signature)); memset(ctx, 0, sizeof(pdkim_ctx)); -ctx->linebuf = store_get(PDKIM_MAX_BODY_LINE_LEN); +ctx->flags = dot_stuffed ? PDKIM_MODE_SIGN | PDKIM_DOT_TERM : PDKIM_MODE_SIGN; +ctx->linebuf = CS (ctx+1); -sig = store_get(sizeof(pdkim_signature)); +DEBUG(D_acl) ctx->dns_txt_callback = dns_txt_callback; + +sig = (pdkim_signature *)(ctx->linebuf + PDKIM_MAX_BODY_LINE_LEN); memset(sig, 0, sizeof(pdkim_signature)); sig->bodylength = -1; - -ctx->mode = PDKIM_MODE_SIGN; ctx->sig = sig; -sig->domain = string_copy(domain); -sig->selector = string_copy(selector); -sig->rsa_privkey = string_copy(rsa_privkey); +sig->domain = string_copy(US domain); +sig->selector = string_copy(US selector); +sig->rsa_privkey = string_copy(US rsa_privkey); sig->algo = algo; -exim_sha_init(&sig->body_hash, algo == PDKIM_ALGO_RSA_SHA1); +exim_sha_init(&sig->body_hash, algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256); + +DEBUG(D_acl) + { + pdkim_signature s = *sig; + ev_ctx vctx; + + debug_printf("PDKIM (checking verify key)<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + if (!pdkim_key_from_dns(ctx, &s, &vctx)) + debug_printf("WARNING: bad dkim key in dns\n"); + debug_printf("PDKIM (finished checking verify key)<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"); + } + return ctx; } @@ -1628,7 +1761,7 @@ pdkim_set_optional(pdkim_ctx *ctx, pdkim_signature * sig = ctx->sig; if (identity) - sig->identity = string_copy(identity); + sig->identity = string_copy(US identity); sig->sign_headers = string_copy(sign_headers ? US sign_headers : US PDKIM_DEFAULT_SIGN_HEADERS);