* PDKIM - a RFC4871 (DKIM) implementation
*
* Copyright (C) 2009 - 2016 Tom Kistner <tom@duncanthrax.net>
- * Copyright (C) 2016 Jeremy Harris <jgh@exim.org>
+ * Copyright (C) 2016 - 2017 Jeremy Harris <jgh@exim.org>
*
* http://duncanthrax.net/pdkim/
*
#endif
#include "pdkim.h"
-#include "rsa.h"
+#include "signing.h"
#define PDKIM_SIGNATURE_VERSION "1"
#define PDKIM_PUB_RECORD_VERSION US "DKIM1"
US"relaxed",
NULL
};
+/*XXX currently unused */
const uschar * pdkim_hashes[] = {
US"sha256",
US"sha1",
NULL
};
+/*XXX currently unused */
const uschar * pdkim_keytypes[] = {
US"rsa",
NULL
{
case PDKIM_VERIFY_FAIL_BODY: return "PDKIM_VERIFY_FAIL_BODY";
case PDKIM_VERIFY_FAIL_MESSAGE: return "PDKIM_VERIFY_FAIL_MESSAGE";
+ case PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH: return "PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH";
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";
}
}
-const char *
+const uschar *
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)";
+ case PDKIM_OK: return US"OK";
+ case PDKIM_FAIL: return US"FAIL";
+ case PDKIM_ERR_RSA_PRIVKEY: return US"RSA_PRIVKEY";
+ case PDKIM_ERR_RSA_SIGNING: return US"RSA SIGNING";
+ case PDKIM_ERR_LONG_LINE: return US"RSA_LONG_LINE";
+ case PDKIM_ERR_BUFFER_TOO_SMALL: return US"BUFFER_TOO_SMALL";
+ case PDKIM_SIGN_PRIVKEY_WRAP: return US"PRIVKEY_WRAP";
+ case PDKIM_SIGN_PRIVKEY_B64D: return US"PRIVKEY_B64D";
+ default: return US"(unknown)";
}
}
pdkim_hexprint(const uschar *data, int len)
{
int i;
-for (i = 0 ; i < len; i++) debug_printf("%02x", data[i]);
+if (data) for (i = 0 ; i < len; i++) debug_printf("%02x", data[i]);
+else debug_printf("<NULL>");
debug_printf("\n");
}
/* Performs "relaxed" canonicalization of a header. */
static uschar *
-pdkim_relax_header(const uschar * header, int crlf)
+pdkim_relax_header(const uschar * header, BOOL append_crlf)
{
BOOL past_field_name = FALSE;
BOOL seen_wsp = FALSE;
for (p = header; *p; p++)
{
uschar c = *p;
- /* Ignore CR & LF */
- if (c == '\r' || c == '\n')
+
+ if (c == '\r' || c == '\n') /* Ignore CR & LF */
continue;
if (c == '\t' || c == ' ')
{
else
if (!past_field_name && c == ':')
{
- if (seen_wsp) q--; /* This removes WSP before the colon */
- seen_wsp = TRUE; /* This removes WSP after the colon */
+ if (seen_wsp) q--; /* This removes WSP immediately before the colon */
+ seen_wsp = TRUE; /* This removes WSP immediately after the colon */
past_field_name = TRUE;
}
else
if (q > relaxed && q[-1] == ' ') q--; /* Squash eventual trailing SP */
-if (crlf) { *q++ = '\r'; *q++ = '\n'; }
+if (append_crlf) { *q++ = '\r'; *q++ = '\n'; }
*q = '\0';
return relaxed;
}
/* -------------------------------------------------------------------------- */
#define PDKIM_QP_ERROR_DECODE -1
-static uschar *
-pdkim_decode_qp_char(uschar *qp_p, int *c)
+static const uschar *
+pdkim_decode_qp_char(const uschar *qp_p, int *c)
{
-uschar *initial_pos = qp_p;
+const uschar *initial_pos = qp_p;
/* Advance one char */
qp_p++;
/* -------------------------------------------------------------------------- */
static uschar *
-pdkim_decode_qp(uschar * str)
+pdkim_decode_qp(const uschar * str)
{
int nchar = 0;
uschar * q;
-uschar * p = str;
+const uschar * p = str;
uschar * n = store_get(Ustrlen(str)+1);
*n = '\0';
/* -------------------------------------------------------------------------- */
static void
-pdkim_decode_base64(uschar *str, blob * b)
+pdkim_decode_base64(const uschar * str, blob * b)
{
int dlen;
dlen = b64decode(str, &b->data);
static pdkim_signature *
pdkim_parse_sig_header(pdkim_ctx *ctx, uschar * raw_hdr)
{
-pdkim_signature *sig ;
+pdkim_signature * sig;
uschar *p, *q;
uschar * cur_tag = NULL; int ts = 0, tl = 0;
uschar * cur_val = NULL; int vs = 0, vl = 0;
switch (*cur_tag)
{
case 'b':
- if (cur_tag[1] == 'h')
- pdkim_decode_base64(cur_val, &sig->bodyhash);
- else
- pdkim_decode_base64(cur_val, &sig->sigdata);
+ pdkim_decode_base64(cur_val,
+ cur_tag[1] == 'h' ? &sig->bodyhash : &sig->sighash);
break;
case 'v':
/* We only support version 1, and that is currently the
Ustrcmp(cur_val, PDKIM_SIGNATURE_VERSION) == 0 ? 1 : -1;
break;
case 'a':
+/*XXX this searches a list of combined (algo + hash-method)s */
for (i = 0; pdkim_algos[i]; i++)
if (Ustrcmp(cur_val, pdkim_algos[i]) == 0)
{
"PDKIM >> Raw signature w/o b= tag value >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
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);
+ "PDKIM >> Sig size: %4u bits\n", (unsigned) sig->sighash.len*8);
debug_printf(
"PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
}
-exim_sha_init(&sig->body_hash, sig->algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256);
+/*XXX hash method: extend for sha512 */
+if (!exim_sha_init(&sig->body_hash_ctx,
+ sig->algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256))
+ {
+ DEBUG(D_acl) debug_printf("PDKIM: hash init internal error\n");
+ return NULL;
+ }
return sig;
}
static pdkim_pubkey *
pdkim_parse_pubkey_record(pdkim_ctx *ctx, const uschar *raw_record)
{
-pdkim_pubkey *pub;
-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;
+const uschar * ele;
+int sep = ';';
+pdkim_pubkey * pub;
pub = store_get(sizeof(pdkim_pubkey));
memset(pub, 0, sizeof(pdkim_pubkey));
-for (p = raw_record; ; p++)
+while ((ele = string_nextinlist(&raw_record, &sep, NULL, 0)))
{
- char c = *p;
+ const uschar * val;
- /* 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 ((val = Ustrchr(ele, '=')))
{
- if (c >= 'a' && c <= 'z')
- cur_tag = string_catn(cur_tag, &ts, &tl, p, 1);
+ int taglen = val++ - ele;
- if (c == '=')
+ DEBUG(D_acl) debug_printf(" %.*s=%s\n", taglen, ele, val);
+ switch (ele[0])
{
- cur_tag[tl] = '\0';
- where = PDKIM_HDR_VALUE;
- goto NEXT_CHAR;
- }
- }
-
- 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);
-
- switch (cur_tag[0])
- {
- 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 (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;
+ case 'v': pub->version = val; break;
+ case 'h': pub->hashes = val; break;
+ case 'k': break;
+ case 'g': pub->granularity = val; break;
+ case 'n': pub->notes = pdkim_decode_qp(val); break;
+ case 'p': pdkim_decode_base64(val, &pub->key); break;
+ case 's': pub->srvtype = val; break;
+ case 't': if (Ustrchr(val, 'y')) pub->testing = 1;
+ if (Ustrchr(val, 's')) pub->no_subdomaining = 1;
+ break;
+ default: DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); break;
}
- else
- cur_val = string_catn(cur_val, &vs, &vl, p, 1);
}
-
-NEXT_CHAR:
- if (c == '\0') break;
}
/* Set fallback defaults */
if (!pub->version ) pub->version = string_copy(PDKIM_PUB_RECORD_VERSION);
-if (!pub->granularity) pub->granularity = string_copy(US"*");
-if (!pub->keytype ) pub->keytype = string_copy(US"rsa");
-if (!pub->srvtype ) pub->srvtype = string_copy(US"*");
+else if (Ustrcmp(pub->version, PDKIM_PUB_RECORD_VERSION) != 0)
+ {
+ DEBUG(D_acl) debug_printf(" Bad v= field\n");
+ return NULL;
+ }
+
+if (!pub->granularity) pub->granularity = US"*";
+/*
+if (!pub->keytype ) pub->keytype = US"rsa";
+*/
+if (!pub->srvtype ) pub->srvtype = US"*";
/* p= is required */
if (pub->key.data)
return pub;
+DEBUG(D_acl) debug_printf(" Missing p= field\n");
return NULL;
}
/* -------------------------------------------------------------------------- */
static int
-pdkim_update_bodyhash(pdkim_ctx *ctx, const char *data, int len)
+pdkim_update_bodyhash(pdkim_ctx * ctx, const char * data, int len)
{
-pdkim_signature *sig = ctx->sig;
-/* Cache relaxed version of data */
-uschar *relaxed_data = NULL;
-int relaxed_len = 0;
+pdkim_signature * sig;
+uschar * relaxed_data = NULL; /* Cache relaxed version of data */
+int relaxed_len = 0;
/* Traverse all signatures, updating their hashes. */
-while (sig)
+for (sig = ctx->sig; sig; sig = sig->next)
{
/* Defaults to simple canon (no further treatment necessary) */
const uschar *canon_data = CUS data;
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++)
{
if (canon_len > 0)
{
- exim_sha_update(&sig->body_hash, CUS canon_data, canon_len);
+ exim_sha_update(&sig->body_hash_ctx, CUS canon_data, canon_len);
sig->signed_body_bytes += canon_len;
DEBUG(D_acl) pdkim_quoteprint(canon_data, canon_len);
}
-
- sig = sig->next;
}
+if (relaxed_data) store_free(relaxed_data);
return PDKIM_OK;
}
{ /* Finish hashes */
blob bh;
- exim_sha_finish(&sig->body_hash, &bh);
+ exim_sha_finish(&sig->body_hash_ctx, &bh);
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;
sig->bodylength = -1;
}
- /* VERIFICATION --------------------------------------------------------- */
else
- {
- /* Compare bodyhash */
- if (memcmp(bh.data, sig->bodyhash.data, bh.len) == 0)
+ /* VERIFICATION --------------------------------------------------------- */
+ /* Be careful that the header sig included a bodyash */
+
+ if (sig->bodyhash.data && memcmp(bh.data, sig->bodyhash.data, bh.len) == 0)
{
DEBUG(D_acl) debug_printf("PDKIM [%s] Body hash verified OK\n", sig->domain);
}
{
DEBUG(D_acl)
{
- debug_printf("PDKIM [%s] bh signature: ", sig->domain);
- pdkim_hexprint(sig->bodyhash.data,
- exim_sha_hashlen(&sig->body_hash));
+ debug_printf("PDKIM [%s] Body hash signature from headers: ", sig->domain);
+ pdkim_hexprint(sig->bodyhash.data, sig->bodyhash.len);
debug_printf("PDKIM [%s] Body hash did NOT verify\n", sig->domain);
}
sig->verify_status = PDKIM_VERIFY_FAIL;
sig->verify_ext_status = PDKIM_VERIFY_FAIL_BODY;
}
- }
}
}
+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)
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 */
#define DKIM_SIGNATURE_HEADERNAME "DKIM-Signature:"
static int
-pdkim_header_complete(pdkim_ctx *ctx)
+pdkim_header_complete(pdkim_ctx * ctx)
{
+pdkim_signature * sig, * last_sig;
+
/* Special case: The last header can have an extra \r appended */
if ( (ctx->cur_header_len > 1) &&
(ctx->cur_header[(ctx->cur_header_len)-1] == '\r') )
--ctx->cur_header_len;
ctx->cur_header[ctx->cur_header_len] = '\0';
-ctx->num_headers++;
-if (ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL;
+if (++ctx->num_headers > PDKIM_MAX_HEADERS) goto BAIL;
/* SIGNING -------------------------------------------------------------- */
-if (ctx->mode == PDKIM_MODE_SIGN)
- {
- pdkim_signature *sig;
-
+if (ctx->flags & PDKIM_MODE_SIGN)
for (sig = ctx->sig; sig; sig = sig->next) /* Traverse all signatures */
/* Add header to the signed headers list (in reverse order) */
sig->headers = pdkim_prepend_stringlist(sig->headers,
ctx->cur_header);
- }
/* VERIFICATION ----------------------------------------------------------- */
/* DKIM-Signature: headers are added to the verification list */
-if (ctx->mode == PDKIM_MODE_VERIFY)
+else
{
+#ifdef notdef
+ DEBUG(D_acl)
+ {
+ debug_printf("PDKIM >> raw hdr: ");
+ pdkim_quoteprint(CUS ctx->cur_header, ctx->cur_header_len);
+ }
+#endif
if (strncasecmp(CCS ctx->cur_header,
DKIM_SIGNATURE_HEADERNAME,
Ustrlen(DKIM_SIGNATURE_HEADERNAME)) == 0)
{
- pdkim_signature *new_sig;
+ /* Create and chain new signature block. We could error-check for all
+ required tags here, but prefer to create the internal sig and expicitly
+ fail verification of it later. */
- /* Create and chain new signature block */
DEBUG(D_acl) debug_printf(
"PDKIM >> Found sig, trying to parse >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
- if ((new_sig = pdkim_parse_sig_header(ctx, ctx->cur_header)))
+ sig = pdkim_parse_sig_header(ctx, ctx->cur_header);
+
+ if (!(last_sig = ctx->sig))
+ ctx->sig = sig;
+ else
{
- pdkim_signature *last_sig = ctx->sig;
- if (!last_sig)
- ctx->sig = new_sig;
- else
- {
- while (last_sig->next) last_sig = last_sig->next;
- last_sig->next = new_sig;
- }
+ while (last_sig->next) last_sig = last_sig->next;
+ last_sig->next = sig;
}
- else
- DEBUG(D_acl) debug_printf(
- "Error while parsing signature header\n"
- "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
}
- /* every other header is stored for signature verification */
- else
- ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header);
+ /* all headers are stored for signature verification */
+ ctx->headers = pdkim_prepend_stringlist(ctx->headers, ctx->cur_header);
}
BAIL:
-*ctx->cur_header = '\0';
-ctx->cur_header_len = 0; /* leave buffer for reuse */
+ctx->cur_header[ctx->cur_header_len = 0] = '\0'; /* leave buffer for reuse */
return PDKIM_OK;
}
#define HEADER_BUFFER_FRAG_SIZE 256
DLLEXPORT int
-pdkim_feed(pdkim_ctx *ctx, char *data, int len)
+pdkim_feed(pdkim_ctx * ctx, uschar * data, int len)
{
-int p;
+int p, rc;
-for (p = 0; p<len; p++)
+/* Alternate EOD signal, used in non-dotstuffing mode */
+if (!data)
+ pdkim_body_complete(ctx);
+
+else for (p = 0; p<len; p++)
{
uschar c = data[p];
- if (ctx->past_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;
+ 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;
- }
- 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;
+ 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) /* Seen last header line */
+ {
+ if ((rc = pdkim_header_complete(ctx)) != 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
+ ctx->flags = (ctx->flags & ~PDKIM_SEEN_CR) | PDKIM_SEEN_LF;
+ }
+ else if (ctx->flags & PDKIM_SEEN_LF)
+ {
+ if (!(c == '\t' || c == ' ')) /* End of header */
+ if ((rc = pdkim_header_complete(ctx)) != PDKIM_OK)
+ return rc;
+ ctx->flags &= ~PDKIM_SEEN_LF;
}
if (ctx->cur_header_len < PDKIM_MAX_HEADER_LEN)
/* Required and static bits */
hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"a=",
+/*XXX this is a combo of algo and hash-method */
pdkim_algos[sig->algo]);
hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"q=",
pdkim_querymethods[sig->querymethod]);
}
/* Preliminary or final version? */
-base64_b = final ? pdkim_encode_base64(&sig->sigdata) : US"";
-hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"b=", base64_b);
+if (final)
+ {
+ base64_b = pdkim_encode_base64(&sig->sighash);
+ hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"b=", base64_b);
-/* add trailing semicolon: I'm not sure if this is actually needed */
-hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, NULL, US";", US"");
+ /* add trailing semicolon: I'm not sure if this is actually needed */
+ hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, NULL, US";", US"");
+ }
+else
+ {
+ /* To satisfy the rule "all surrounding whitespace [...] deleted"
+ ( RFC 6376 section 3.7 ) we ensure there is no whitespace here. Otherwise
+ the headcat routine could insert a linebreak which the relaxer would reduce
+ to a single space preceding the terminating semicolon, resulting in an
+ incorrect header-hash. */
+ hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"b=;", US"");
+ }
hdr[hdr_len] = '\0';
return hdr;
}
+/* -------------------------------------------------------------------------- */
+
+static pdkim_pubkey *
+pdkim_key_from_dns(pdkim_ctx * ctx, pdkim_signature * sig, ev_ctx * vctx,
+ const uschar ** errstr)
+{
+uschar * dns_txt_name, * dns_txt_reply;
+pdkim_pubkey * p;
+
+/* 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_dkim_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_feed_finish(pdkim_ctx * ctx, pdkim_signature ** return_signatures,
+ const uschar ** err)
{
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
while (sig)
{
+/*XXX bool probably not enough */
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;
hdata.data = NULL;
hdata.len = 0;
- exim_sha_init(&hhash_ctx, is_sha1 ? HASH_SHA1 : HASH_SHA256);
+ if (!exim_sha_init(&hhash_ctx, is_sha1 ? HASH_SHA1 : HASH_SHA256))
+ {
+ DEBUG(D_acl) debug_printf("PDKIM: hask setup internal error\n");
+ break;
+ }
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
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;
p->value, (q - US p->value) + (p->next ? 1 : 0));
rh = sig->canon_headers == PDKIM_CANON_RELAXED
- ? pdkim_relax_header(p->value, 1) /* cook header for relaxed canon */
+ ? pdkim_relax_header(p->value, TRUE) /* cook header for relaxed canon */
: string_copy(CUS p->value); /* just copy it for simple canon */
/* Feed header to the hash algorithm */
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);
+ (void) exim_dkim_data_append(&hdata, &hdata_alloc, rh);
DEBUG(D_acl) pdkim_quoteprint(rh, Ustrlen(rh));
}
/* 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);
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, CUS 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, TRUE)
+ : 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(
/* Relax header if necessary */
if (sig->canon_headers == PDKIM_CANON_RELAXED)
- sig_hdr = pdkim_relax_header(sig_hdr, 0);
+ sig_hdr = pdkim_relax_header(sig_hdr, FALSE);
DEBUG(D_acl)
{
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)
- (void) exim_rsa_data_append(&hdata, &hdata_alloc, US sig_hdr);
+/*XXX is this assuing algo == RSA? */
+ if (ctx->flags & PDKIM_MODE_SIGN)
+ (void) exim_dkim_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;
/* Import private key */
- if ((errstr = exim_rsa_signing_init(US sig->rsa_privkey, &sctx)))
+/*XXX extend for non-RSA algos */
+ if ((*err = exim_dkim_signing_init(US sig->rsa_privkey, &sctx)))
{
- DEBUG(D_acl) debug_printf("signing_init: %s\n", errstr);
+ DEBUG(D_acl) debug_printf("signing_init: %s\n", *err);
return PDKIM_ERR_RSA_PRIVKEY;
}
hdata = hhash;
#endif
- if ((errstr = exim_rsa_sign(&sctx, is_sha1, &hdata, &sig->sigdata)))
+/*XXX extend for non-RSA algos */
+/*XXX oddly the dkim rfc does _not_ say what variant (sha1 or sha256) of
+RSA signing should be done. We use the same variant as the hash-method. */
+
+ if ((*err = exim_dkim_sign(&sctx, is_sha1, &hdata, &sig->sighash)))
{
- DEBUG(D_acl) debug_printf("signing: %s\n", errstr);
+ DEBUG(D_acl) debug_printf("signing: %s\n", *err);
return PDKIM_ERR_RSA_SIGNING;
}
DEBUG(D_acl)
{
debug_printf( "PDKIM [%s] b computed: ", sig->domain);
- pdkim_hexprint(sig->sigdata.data, sig->sigdata.len);
+ pdkim_hexprint(sig->sighash.data, sig->sighash.len);
}
sig->signature_header = pdkim_create_header(sig, TRUE);
else
{
ev_ctx vctx;
- const uschar * errstr;
-
- uschar *dns_txt_name, *dns_txt_reply;
/* 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->sighash.data
&& sig->algo > -1
&& sig->version
) )
goto NEXT_VERIFY;
}
- /* 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;
- goto NEXT_VERIFY;
- }
-
- DEBUG(D_acl)
- {
- debug_printf(
- "PDKIM >> Parsing public key record >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
- " Raw record: ");
- pdkim_quoteprint(CUS dns_txt_reply, Ustrlen(dns_txt_reply));
- }
-
- if (!(sig->pubkey = pdkim_parse_pubkey_record(ctx, CUS dns_txt_reply)))
- {
- sig->verify_status = PDKIM_VERIFY_INVALID;
- sig->verify_ext_status = PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD;
-
- DEBUG(D_acl) debug_printf(
- " Error while parsing public key record\n"
- "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
+ if (!(sig->pubkey = pdkim_key_from_dns(ctx, sig, &vctx, err)))
goto NEXT_VERIFY;
- }
- DEBUG(D_acl) debug_printf(
- "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
+ /* If the pubkey limits to a list of specific hashes, ignore sigs that
+ do not have the hash part of the sig algorithm matching */
- /* Import public key */
- if ((errstr = exim_rsa_verify_init(&sig->pubkey->key, &vctx)))
+ if (sig->pubkey->hashes)
{
- 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;
- goto NEXT_VERIFY;
+ const uschar * list = sig->pubkey->hashes, * ele;
+ int sep = ':';
+ while ((ele = string_nextinlist(&list, &sep, NULL, 0)))
+ if (Ustrcmp(ele, pdkim_algos[sig->algo] + 4) == 0) break;
+ if (!ele)
+ {
+ DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%s\n",
+ sig->pubkey->hashes, pdkim_algos[sig->algo]);
+ sig->verify_status = PDKIM_VERIFY_FAIL;
+ sig->verify_ext_status = PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH;
+ goto NEXT_VERIFY;
+ }
}
/* Check the signature */
- if ((errstr = exim_rsa_verify(&vctx, is_sha1, &hhash, &sig->sigdata)))
+/*XXX needs extension for non-RSA */
+ if ((*err = exim_dkim_verify(&vctx, is_sha1, &hhash, &sig->sighash)))
{
- DEBUG(D_acl) debug_printf("headers verify: %s\n", errstr);
+ DEBUG(D_acl) debug_printf("headers verify: %s\n", *err);
sig->verify_status = PDKIM_VERIFY_FAIL;
sig->verify_ext_status = PDKIM_VERIFY_FAIL_MESSAGE;
goto NEXT_VERIFY;
}
- /* 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;
/* -------------------------------------------------------------------------- */
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;
/* -------------------------------------------------------------------------- */
+/*XXX ? needs extension to cover non-RSA algo? Currently the "algo" is actually
+the combo of algo and hash-method */
+
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 *),
+ const uschar ** errstr)
{
-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(US domain);
sig->rsa_privkey = string_copy(US rsa_privkey);
sig->algo = algo;
-exim_sha_init(&sig->body_hash, algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256);
+/*XXX extend for sha512 */
+if (!exim_sha_init(&sig->body_hash_ctx,
+ algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256))
+ {
+ DEBUG(D_acl) debug_printf("PDKIM: hash setup internal error\n");
+ return NULL;
+ }
+
+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, errstr))
+ debug_printf("WARNING: bad dkim key in dns\n");
+ debug_printf("PDKIM (finished checking verify key)<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
+ }
return ctx;
}
void
pdkim_init(void)
{
-exim_rsa_init();
+exim_dkim_init();
}