aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org>2020-07-06 14:24:54 +0300
committerSergey Poznyakoff <gray@gnu.org>2020-07-06 16:07:53 +0300
commite66164eb05b03be03ccf6187f5b09ca8aba483cf (patch)
tree88050c4c2ac64472784e175977575e22c4500a67 /src
parent2d56241f89eae839959e538cede756539adba89e (diff)
downloadmailfromd-e66164eb05b03be03ccf6187f5b09ca8aba483cf.tar.gz
mailfromd-e66164eb05b03be03ccf6187f5b09ca8aba483cf.tar.bz2
Implement DKIM verification
* lib/dns.c (dkim_lookup): New function. * lib/dns.h (dkim_lookup): New proto. * mflib/status.mf (DKIM_VERIFY_OK, DKIM_VERIFY_PERMFAIL) (DKIM_VERIFY_TEMPFAIL): New constants. * src/builtin/dkim.bi (msgmod_data) <h_list>: New member. (do_msgmod): Ignore modification commands that affect headers not listed in h=. (dkim_sign): Initialize v, a, and q members of struct dkim_signature. (dkim_explanation_code, dkim_explanation): New MFL variables. (dkim_verify): New function. * src/dkim.c (pubkey_from_base64): New function. (dkim_header_list_match): New function. (dkim_signature_format): Format new members of the struct dkim_signature. (dkim_signature_free): New function. (dkim_signature_parse): New function. (dkim_hash): New function. (mfd_dkim_sign): Rewrite using dkim_hash. (dkim_explanation_str) (dkim_result_trans): New globals. (mfd_dkim_verify): New function. * src/dkim.h (dkim_header_list_match): New proto. (mfd_dkim_verify): New proto. (DKIM_VERSION,DKIM_SIGNATURE_HEADER) (DKIM_QUERY_METHOD,DKIM_ALGORITHM): New constants. (dkim_signature) <a,q,v>: New members. (DKIM_EXPL_*): New constants. (DKIM_VERIFY_*): New constants. (dkim_explanation_str, dkim_result_trans): New externs. * NEWS: Document changes. * doc/functions.texi: Likewise.
Diffstat (limited to 'src')
-rw-r--r--src/builtin/dkim.bi42
-rw-r--r--src/dkim.c719
-rw-r--r--src/dkim.h48
3 files changed, 738 insertions, 71 deletions
diff --git a/src/builtin/dkim.bi b/src/builtin/dkim.bi
index a0b07234..52ac4e3c 100644
--- a/src/builtin/dkim.bi
+++ b/src/builtin/dkim.bi
@@ -21,6 +21,7 @@ MF_BUILTIN_MODULE
struct msgmod_data {
mu_message_t msg;
+ char const *h_list;
struct msgmod_closure *e_msgmod;
};
@@ -69,6 +70,8 @@ do_msgmod(void *item, void *data)
switch (msgmod->opcode) {
case header_replace:
+ if (!dkim_header_list_match(md->h_list, msgmod->name))
+ break;
rc = mu_header_insert(hdr, msgmod->name, msgmod->value,
NULL,
msgmod->idx, MU_HEADER_REPLACE);
@@ -93,6 +96,8 @@ do_msgmod(void *item, void *data)
break;
case header_delete:
+ if (!dkim_header_list_match(md->h_list, msgmod->name))
+ break;
rc = mu_header_remove(hdr, msgmod->name, msgmod->idx);
if (rc && rc != MU_ERR_NOENT) {
mu_diag_funcall(MU_DIAG_ERROR,
@@ -107,11 +112,14 @@ do_msgmod(void *item, void *data)
case body_repl:
rc = mu_static_memory_stream_create(&stream, msgmod->value,
strlen(msgmod->value));
- if (rc)
+ if (rc) {
+ md->e_msgmod = msgmod;
return rc;
+ }
rc = message_replace_body(md->msg, stream);
if (rc) {
mu_stream_destroy(&stream);
+ md->e_msgmod = msgmod;
return rc;
}
break;
@@ -120,17 +128,22 @@ do_msgmod(void *item, void *data)
rc = mu_fd_stream_create (&stream, "<body_repl_fd>",
msgmod->idx,
MU_STREAM_READ);
- if (rc)
+ if (rc) {
+ md->e_msgmod = msgmod;
return rc;
+ }
rc = message_replace_body(md->msg, stream);
if (rc) {
mu_stream_destroy(&stream);
+ md->e_msgmod = msgmod;
return rc;
}
break;
case header_add:
case header_insert:
+ if (!dkim_header_list_match(md->h_list, msgmod->name))
+ break;
md->e_msgmod = msgmod;
return MU_ERR_USER0;
@@ -142,8 +155,6 @@ do_msgmod(void *item, void *data)
}
return 0;
}
-
-
MF_STATE(eom)
MF_CAPTURE
@@ -152,9 +163,12 @@ OPTIONAL,
STRING canon_h, STRING canon_b, STRING headers)
{
struct dkim_signature sig = {
+ .v = DKIM_VERSION,
+ .a = DKIM_ALGORITHM,
.d = d,
.s = s,
- .canon = { DKIM_CANON_SIMPLE, DKIM_CANON_SIMPLE }
+ .canon = { DKIM_CANON_SIMPLE, DKIM_CANON_SIMPLE },
+ .q = DKIM_QUERY_METHOD
};
static char default_headers[] =
"From:From:"
@@ -210,6 +224,7 @@ STRING canon_h, STRING canon_b, STRING headers)
"mu_message_create_copy: %s",
mu_strerror(rc));
}
+ mdat.h_list = sig.h;
mdat.e_msgmod = 0;
rc = env_msgmod_apply(env, do_msgmod, &mdat);
if (rc) {
@@ -238,9 +253,7 @@ STRING canon_h, STRING canon_b, STRING headers)
rc = mfd_dkim_sign(msg, &sig, keyfile, &sighdr);
mu_message_unref(msg);
- MF_ASSERT(rc == 0,
- mfe_failure,
- _("DKIM failed"));
+ MF_ASSERT(rc == 0, mfe_failure, _("DKIM failed"));
p = strchr(sighdr, ':');
*p++ = 0;
@@ -256,3 +269,16 @@ STRING canon_h, STRING canon_b, STRING headers)
free(sighdr);
}
END
+
+MF_VAR(dkim_explanation_code, NUMBER);
+MF_VAR(dkim_explanation, STRING);
+
+MF_DEFUN(dkim_verify, NUMBER, NUMBER nmsg)
+{
+ mu_message_t msg = bi_message_from_descr(env, nmsg);
+ int result = mfd_dkim_verify(msg);
+ MF_VAR_REF(dkim_explanation_code, long, result);
+ MF_VAR_SET_STRING(dkim_explanation, dkim_explanation_str[result]);
+ MF_RETURN(dkim_result_trans[result]);
+}
+END
diff --git a/src/dkim.c b/src/dkim.c
index f7fffed2..bc3c3da8 100644
--- a/src/dkim.c
+++ b/src/dkim.c
@@ -25,6 +25,7 @@
#include <nettle/buffer.h>
#include <nettle/rsa.h>
#include <nettle/base64.h>
+#include <nettle/asn1.h>
#include "mailfromd.h"
#include <mailutils/imaputil.h>
#include "dkim.h"
@@ -232,6 +233,99 @@ read_keys(FILE *fp, struct rsa_public_key *pub, struct rsa_private_key *priv)
return rc;
}
+
+static int
+pubkey_from_base64(struct rsa_public_key *pub, const char *str)
+{
+ struct nettle_buffer buffer;
+ size_t length = strlen(str);
+ struct asn1_der_iterator i, j;
+ int result = READ_PEM_ERROR;
+
+ nettle_buffer_init_realloc(&buffer, NULL, nettle_xrealloc);
+ nettle_buffer_write(&buffer, strlen(str), (const uint8_t*) str);
+ if (decode_base64(&buffer, 0, &length) == READ_PEM_OK
+
+ /* SubjectPublicKeyInfo ::= SEQUENCE {
+ algorithm AlgorithmIdentifier,
+ subjectPublicKey BIT STRING
+ }
+
+ AlgorithmIdentifier ::= SEQUENCE {
+ algorithm OBJECT IDENTIFIER,
+ parameters OPTIONAL
+ }
+ */
+
+ && asn1_der_iterator_first(&i, length, buffer.contents) == ASN1_ITERATOR_CONSTRUCTED
+ && i.type == ASN1_SEQUENCE
+ && asn1_der_decode_constructed_last(&i) == ASN1_ITERATOR_CONSTRUCTED
+ && i.type == ASN1_SEQUENCE
+
+ /* Use the j iterator to parse the algorithm identifier */
+ && asn1_der_decode_constructed(&i, &j) == ASN1_ITERATOR_PRIMITIVE
+ && j.type == ASN1_IDENTIFIER
+ && asn1_der_iterator_next(&i) == ASN1_ITERATOR_PRIMITIVE
+ && i.type == ASN1_BITSTRING
+
+ /* Use i to parse the object wrapped in the bit string.*/
+ && asn1_der_decode_bitstring_last(&i)) {
+ /* pkcs-1 {
+ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+ pkcs-1(1) modules(0) pkcs-1(1)
+ }
+
+ --
+ -- When rsaEncryption is used in an AlgorithmIdentifier the
+ -- parameters MUST be present and MUST be NULL.
+ --
+ rsaEncryption OBJECT IDENTIFIER ::= { pkcs-1 1 }
+ */
+ static const uint8_t id_rsaEncryption[9] =
+ { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
+
+ if (j.length == sizeof(id_rsaEncryption)
+ && memcmp(j.data, id_rsaEncryption,
+ sizeof(id_rsaEncryption)) == 0
+ && asn1_der_iterator_next(&j) == ASN1_ITERATOR_PRIMITIVE
+ && j.type == ASN1_NULL
+ && j.length == 0
+ && asn1_der_iterator_next(&j) == ASN1_ITERATOR_END) {
+ rsa_public_key_init(pub);
+
+ if (rsa_public_key_from_der_iterator(pub, 0, &i))
+ result = READ_PEM_OK;
+ }
+ }
+ nettle_buffer_clear(&buffer);
+ return result;
+}
+
+int
+dkim_header_list_match(char const *h_list, char const *h)
+{
+ size_t len = strlen (h);
+ while (*h_list) {
+ size_t n;
+
+ while (*h_list && (*h_list == ' ' || *h_list == '\t'))
+ h_list++;
+ if (*h_list == 0)
+ break;
+
+ n = strcspn(h_list, " \t:");
+ if (n == len && mu_c_strncasecmp (h_list, h, len) == 0)
+ return 1;
+ h_list += n;
+
+ while (*h_list && (*h_list == ' ' || *h_list == '\t'))
+ h_list++;
+ if (*h_list != ':')
+ break;
+ ++h_list;
+ }
+ return 0;
+}
/*
* SHA256 hashing functions.
@@ -385,8 +479,12 @@ dkim_signature_format(struct dkim_signature *sig, char **result)
rc = mu_opool_create(&op, MU_OPOOL_DEFAULT);
if (rc)
return rc;
- mu_opool_appendz(op, "DKIM-Signature: ");
- mu_opool_appendz(op, "v=1; a=rsa-sha256; d=");
+ mu_opool_appendz(op, DKIM_SIGNATURE_HEADER ": ");
+ mu_opool_appendz(op, "v=");
+ mu_opool_appendz(op, sig->v);
+ mu_opool_appendz(op, "; a=");
+ mu_opool_appendz(op, sig->a ? sig->a : DKIM_ALGORITHM);
+ mu_opool_appendz(op, "; d=");
mu_opool_appendz(op, sig->d);
mu_opool_appendz(op, "; s=");
mu_opool_appendz(op, sig->s);
@@ -399,7 +497,9 @@ dkim_signature_format(struct dkim_signature *sig, char **result)
mu_opool_appendz(op, dkim_canon_string[sig->canon[1]]);
mu_opool_appendz(op, "; ");
}
- mu_opool_appendz(op, "q=dns/txt; ");
+ mu_opool_appendz(op, "q=");
+ mu_opool_appendz(op, sig->q ? sig->q : DKIM_QUERY_METHOD);
+ mu_opool_appendz(op, "; ");
if (sig->i) {
mu_opool_appendz(op, "i=");
mu_opool_appendz(op, sig->i);
@@ -435,6 +535,114 @@ dkim_signature_format(struct dkim_signature *sig, char **result)
mu_opool_destroy(&op);
return 0;
}
+
+void
+dkim_signature_free(struct dkim_signature *sig)
+{
+ free(sig->a);
+ free(sig->b);
+ free(sig->bh);
+ free(sig->d);
+ free(sig->s);
+ free(sig->h);
+ free(sig->i);
+ free(sig->q);
+ free(sig->v);
+}
+
+static int
+str_to_time(char const *str, time_t *pt)
+{
+ unsigned long n;
+ char *p;
+ errno = 0;
+ n = strtoul(str, &p, 10);
+ if (errno || *p)
+ return -1;
+ *pt = n;
+ return 0;
+}
+
+int
+dkim_signature_parse(char *str, struct dkim_signature *ret_sig)
+{
+ struct mu_wordsplit ws;
+ int i;
+ int rc = 0;
+ struct dkim_signature sig;
+
+ ws.ws_delim = ";";
+ rc = mu_wordsplit(str,
+ &ws,
+ MU_WRDSF_DELIM |
+ MU_WRDSF_NOVAR |
+ MU_WRDSF_WS |
+ WRDSF_NOCMD);
+ if (rc) {
+ mu_wordsplit_free(&ws);
+ return rc;
+ }
+
+ memset(&sig, 0, sizeof(sig));
+ sig.canon[0] = sig.canon[1] = DKIM_CANON_SIMPLE;
+ for (i = 0; i < ws.ws_wordc; i++) {
+ char *k = ws.ws_wordv[i];
+ char *p = strchr(k, '=');
+ if (!p) {
+ rc = -1;
+ goto end;
+ }
+ *p++ = 0;
+ if (strcmp(k, "a") == 0) {
+ sig.a = (char*) mu_strdup(p);
+ } if (strcmp(k, "b") == 0) {
+ sig.b = (uint8_t*) mu_strdup(p);
+ } else if (strcmp(k, "bh") == 0) {
+ sig.bh = (uint8_t*) mu_strdup(p);
+ } else if (strcmp(k, "q") == 0) {
+ sig.q = mu_strdup(p);
+ } else if (strcmp(k, "c") == 0) {
+ char *s = strchr(p, '/');
+ if (!s) {
+ rc = -1;
+ goto end;
+ }
+ *s++ = 0;
+ if ((sig.canon[0] = dkim_str_to_canon_type(p)) == DKIM_CANON_ERR ||
+ (sig.canon[1] = dkim_str_to_canon_type(s)) == DKIM_CANON_ERR) {
+ rc = -1;
+ goto end;
+ }
+ } else if (strcmp(k, "d") == 0) {
+ sig.d = mu_strdup(p);
+ } else if (strcmp(k, "s") == 0) {
+ sig.s = mu_strdup(p);
+ } else if (strcmp(k, "h") == 0) {
+ sig.h = mu_strdup(p);
+ } else if (strcmp(k, "i") == 0) {
+ sig.i = mu_strdup(p);
+ } else if (strcmp(k, "t") == 0) {
+ if (str_to_time(p, &sig.t)) {
+ rc = -1;
+ goto end;
+ }
+ } else if (strcmp(k, "x") == 0) {
+ if (str_to_time(p, &sig.x)) {
+ rc = -1;
+ goto end;
+ }
+ } else if (strcmp(k, "v") == 0) {
+ sig.v = mu_strdup(p);
+ }
+ }
+end:
+ mu_wordsplit_free(&ws);
+ if (rc)
+ dkim_signature_free(&sig);
+ else
+ *ret_sig = sig;
+ return rc;
+}
/* Canonicalize the message into a stream.
* Arguments:
@@ -583,7 +791,7 @@ is_rev_header(char const *name)
{
static char *rev_headers[] = {
"Received",
- "DKIM-Signature",
+ DKIM_SIGNATURE_HEADER,
"Resent-*",
NULL
};
@@ -596,56 +804,77 @@ is_rev_header(char const *name)
return 0;
}
-/* Sign the message. Arguments:
+static int
+dkim_tag_find(char const *sigstr, char const *tag, size_t *ret_len)
+{
+ int i;
+ size_t tag_len = strlen(tag);
+ size_t sig_len = strlen(sigstr);
+
+ for (i = 0; i + tag_len + 1 < sig_len; i++) {
+ if (!(sigstr[i] == ' ' || sigstr[i] == '\t')) {
+ size_t n = strcspn(sigstr + i, ";");
+ if (memcmp(sigstr + i, tag, tag_len) == 0 &&
+ sigstr[i + tag_len] == '=') {
+ *ret_len = n - tag_len - 1;
+ return i;
+ }
+ i += n;
+ }
+ }
+ return -1;
+}
+
+enum {
+ DKIM_HASH_ERR = -1,
+ DKIM_HASH_OK,
+ DKIM_HASH_DIFF
+};
+
+#define BH_SIZE (BASE64_ENCODE_RAW_LENGTH(SHA256_DIGEST_SIZE))
+
+/*
+ * dkim_hash(MSG, SIG, SIGSTR, CTX)
+ * --------------------------------
+ * Compute a message hash of MSG as per RFC 6376 section 3.7.
*
- * msg message to sign.
- * sig initialized struct dkim_signature.
- * priv_file name of a disk file with the RSA private key in PEM format.
- * ret_sighdr return pointer.
+ * Parameters:
+ * MSG - (input) message
+ * SIG - (input/output) parsed out DKIM signature
+ * SIGSTR - (input) original value of the DKIM signature header.
+ * CTX - (output) SHA256 context to leave the hash in.
*
- * On success, a malloced copy of DKIM-Signature header line will be stored
- * in ret_sighdr and 0 will be returned.
+ * The function is used both for message signing and verification.
*
- * Side effects: sig->bh is filled with SHA256 digest of the message body.
+ * When signing, SIGSTR is NULL. Before return, the malloced copy
+ * of the computed body hash is left in SIG->bh. The caller is
+ * responsible for freeing it when no longer needed. In this mode,
+ * the function returns DKIM_HASH_OK on success and DKIM_HASH_ERR
+ * on error.
+ *
+ * When verifying, SIGSTR is not NULL and SIG is the validated broken
+ * out DKIM signature obtained from SIGSTR. In this case, the function
+ * does not modify SIG in any way. Instead, it checks whether the computed
+ * body hash matches SIG->bh and returns DKIM_HASH_DIFF if it does not.
*/
-int
-mfd_dkim_sign(mu_message_t msg, struct dkim_signature *sig,
- char *priv_file,
- char **ret_sighdr)
+static int
+dkim_hash(mu_message_t msg, struct dkim_signature *sig, char const *sigstr,
+ struct sha256_ctx *ctx)
{
mu_stream_t canon_stream = NULL;
struct mu_wordsplit ws;
struct header_map h_all = HEADER_MAP_INITIALIZER(h_all);
struct header_map h_sel = HEADER_MAP_INITIALIZER(h_sel);
struct header_map *hmap;
- uint8_t bh[BASE64_ENCODE_RAW_LENGTH(SHA256_DIGEST_SIZE)+1];
+ uint8_t bh[BH_SIZE];
mu_opool_t op = NULL;
- int rc;
char c;
- struct sha256_ctx ctx;
- int result = -1;
+ int rc;
+ int result = DKIM_HASH_ERR;
size_t count, i;
enum { H_INIT, H_HEADER, H_CR1, H_CR2, H_NL } state = H_INIT;
- char *sigstr;
mu_stream_t sigcanon, str;
- struct rsa_private_key priv;
- struct rsa_public_key pub;
- FILE *fp;
-
- fp = fopen(priv_file, "r");
- if (!fp) {
- mu_error(_("can't open %s: %s"), priv_file, strerror(errno));
- return -1;
- }
-
- rc = read_keys(fp, &pub, &priv);
- fclose(fp);
- if (rc != READ_PEM_OK) {
- mu_error(_("can't read private key from %s: %s"),
- priv_file, strerror(errno));
- return -1;
- }
- rsa_public_key_clear(&pub);
+ char *sig_str_buf;
/* Create a canonical representation of the message */
if (canonicalize(msg, sig->canon, &canon_stream))
@@ -661,12 +890,6 @@ mfd_dkim_sign(mu_message_t msg, struct dkim_signature *sig,
goto err;
}
- hmap = calloc(1, sizeof(hmap[0]));
- if (!hmap) {
- mu_error(_("not enough memory"));
- goto err;
- }
-
/*
* Scan the header part of the canonicalized stream and record
* the headers in the h_all list.
@@ -675,6 +898,7 @@ mfd_dkim_sign(mu_message_t msg, struct dkim_signature *sig,
* pool.
*/
mu_opool_create(&op, MU_OPOOL_DEFAULT);
+ hmap = NULL;
while ((rc = mu_stream_read(canon_stream, &c, 1, &count)) == 0 &&
count == 1) {
@@ -768,22 +992,49 @@ end:
}
/* Hash the body */
- sig->bh = bh;
- if (dkim_body_hash(canon_stream, sig->bh))
+ if (dkim_body_hash(canon_stream, bh))
goto err;
- bh[BASE64_ENCODE_RAW_LENGTH(SHA256_DIGEST_SIZE)] = 0;
+ if (sig->bh) {
+ if (memcmp(sig->bh, bh, BH_SIZE)) {
+ result = DKIM_HASH_DIFF;
+ goto err;
+ }
+ } else {
+ sig->bh = malloc(BH_SIZE + 1);
+ if (!sig->bh)
+ goto err;
+ memcpy(sig->bh, bh, BH_SIZE);
+ sig->bh[BH_SIZE] = 0;
+ }
/* Hash the selected headers */
- sha256_init(&ctx);
HEADER_MAP_FOREACH(hmap, &h_sel) {
mu_stream_seek(canon_stream, hmap->start, MU_SEEK_SET, NULL);
hash_stream_segment(canon_stream, hmap->end - hmap->start,
- &ctx);
+ ctx);
}
/* Add to the hash the DKIM-Signature header with empty b= tag. */
- dkim_signature_format(sig, &sigstr);
- mu_fixed_memory_stream_create(&str, sigstr, strlen(sigstr),
+ if (sigstr) {
+ size_t len, blen;
+ char *vp;
+ int n = dkim_tag_find(sigstr, "b", &blen);
+ if (n == -1)
+ goto err;
+ len = strlen(sigstr);
+ sig_str_buf = malloc(sizeof(DKIM_SIGNATURE_HEADER) + 1
+ + len - blen + 1);
+ if (!sig_str_buf)
+ goto err;
+ strcpy(sig_str_buf, DKIM_SIGNATURE_HEADER ": ");
+ vp = sig_str_buf + sizeof(DKIM_SIGNATURE_HEADER) + 1;
+ memcpy(vp, sigstr, n);
+ memcpy(vp + n, "b=", 2);
+ strcpy(vp + n + 2, sigstr + n + 2 + blen);
+ } else {
+ dkim_signature_format(sig, &sig_str_buf);
+ }
+ mu_fixed_memory_stream_create(&str, sig_str_buf, strlen(sig_str_buf),
MU_STREAM_RDWR|MU_STREAM_SEEK);
rc = dkim_canonicalizer_create(&sigcanon, str,
sig->canon[0], sig->canon[1],
@@ -793,25 +1044,367 @@ end:
mu_error("dkim_canonicalizer_create: %s", mu_strerror(rc));
goto err;
}
- hash_stream(sigcanon, &ctx);
+ hash_stream(sigcanon, ctx);
mu_stream_unref(sigcanon);
- free(sigstr);
-
- /* Finally, create the RSA-SHA256 signature in b */
- dkim_rsa_sha256_sign(&priv, &ctx, &sig->b);
-
- /* Create the header */
- dkim_signature_format(sig, ret_sighdr);
- result = 0;
+ free(sig_str_buf);
+ result = DKIM_HASH_OK;
err:
/* Reclaim the allocated memory. */
- free(sig->b);
- sig->b = NULL;
mu_opool_destroy(&op);
header_map_free(&h_all);
header_map_free(&h_sel);
mu_wordsplit_free(&ws);
mu_stream_destroy(&canon_stream);
+ return result;
+}
+
+
+/* Sign the message. Arguments:
+ *
+ * msg message to sign.
+ * sig initialized struct dkim_signature.
+ * priv_file name of a disk file with the RSA private key in PEM format.
+ * ret_sighdr return pointer.
+ *
+ * On success, a malloced copy of DKIM-Signature header line will be stored
+ * in ret_sighdr and 0 will be returned.
+ *
+ * Side effects: sig->bh is filled with SHA256 digest of the message body.
+ */
+int
+mfd_dkim_sign(mu_message_t msg, struct dkim_signature *sig,
+ char *priv_file,
+ char **ret_sighdr)
+{
+ int rc;
+ struct rsa_private_key priv;
+ struct rsa_public_key pub;
+ FILE *fp;
+ struct sha256_ctx ctx;
+ int result = -1;
+
+ fp = fopen(priv_file, "r");
+ if (!fp) {
+ mu_error(_("can't open %s: %s"), priv_file, strerror(errno));
+ return -1;
+ }
+
+ rc = read_keys(fp, &pub, &priv);
+ fclose(fp);
+ if (rc != READ_PEM_OK) {
+ mu_error(_("can't read private key from %s: %s"),
+ priv_file, strerror(errno));
+ return -1;
+ }
+ rsa_public_key_clear(&pub);
+
+ sha256_init(&ctx);
+ if (dkim_hash(msg, sig, NULL, &ctx) == DKIM_HASH_OK) {
+ /* Create the RSA-SHA256 signature in b */
+ dkim_rsa_sha256_sign(&priv, &ctx, &sig->b);
+
+ /* Create the header */
+ dkim_signature_format(sig, ret_sighdr);
+ result = 0;
+ }
+ /* Reclaim the allocated memory. */
+ free(sig->b);
+ sig->b = NULL;
+ free(sig->bh);
+ sig->bh = NULL;
rsa_private_key_clear(&priv);
return result;
}
+
+char const *dkim_explanation_str[] = {
+ [DKIM_EXPL_OK] = "DKIM verification passed",
+ [DKIM_EXPL_NO_SIG] = "No DKIM signature",
+ [DKIM_EXPL_INTERNAL_ERROR] = "internal error",
+ [DKIM_EXPL_SIG_SYNTAX] = "signature syntax error",
+ [DKIM_EXPL_SIG_MISS] = "signature is missing required tag",
+ [DKIM_EXPL_DOMAIN_MISMATCH] = "domain mismatch",
+ [DKIM_EXPL_BAD_VERSION] = "incompatible version",
+ [DKIM_EXPL_BAD_ALGORITHM] = "unsupported algorithm",
+ [DKIM_EXPL_BAD_QUERY] = "unsupported query method",
+ [DKIM_EXPL_FROM] = "From field not signed",
+ [DKIM_EXPL_EXPIRED] = "signature expired",
+ [DKIM_EXPL_DNS_UNAVAIL] = "public key unavailable",
+ [DKIM_EXPL_DNS_NOTFOUND] = "public key not found",
+ [DKIM_EXPL_KEY_SYNTAX] = "key syntax error",
+ [DKIM_EXPL_KEY_REVOKED] = "key revoked",
+ [DKIM_EXPL_BAD_BODY] = "body hash did not verify",
+ [DKIM_EXPL_BAD_BASE64] = "can't decode base64",
+ [DKIM_EXPL_BAD_SIG] = "signature did not verify",
+};
+
+int dkim_result_trans[] = {
+ [DKIM_EXPL_OK] = DKIM_VERIFY_OK,
+ [DKIM_EXPL_BAD_ALGORITHM] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_BAD_BASE64] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_BAD_BODY] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_BAD_SIG] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_BAD_QUERY] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_BAD_VERSION] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_DNS_NOTFOUND] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_DOMAIN_MISMATCH] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_EXPIRED] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_FROM] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_KEY_REVOKED] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_KEY_SYNTAX] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_SIG_MISS] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_SIG_SYNTAX] = DKIM_VERIFY_PERMFAIL,
+ [DKIM_EXPL_NO_SIG] = DKIM_VERIFY_TEMPFAIL,
+ [DKIM_EXPL_DNS_UNAVAIL] = DKIM_VERIFY_TEMPFAIL,
+ [DKIM_EXPL_INTERNAL_ERROR] = DKIM_VERIFY_TEMPFAIL,
+};
+
+static int
+dkim_sig_validate(struct dkim_signature const *sig)
+{
+ if (!sig->a
+ || !sig->b
+ || !sig->bh
+ || !sig->d
+ || !sig->h
+ || !sig->s
+ || !sig->q
+ || !sig->v) {
+ return DKIM_EXPL_SIG_MISS;
+ }
+
+ if (strcmp(sig->v, DKIM_VERSION))
+ return DKIM_EXPL_BAD_VERSION;
+
+ if (strcmp(sig->a, DKIM_ALGORITHM))
+ return DKIM_EXPL_BAD_ALGORITHM;
+
+ if (strcmp(sig->q, DKIM_QUERY_METHOD))
+ return DKIM_EXPL_BAD_QUERY;
+
+ if (sig->i) {
+ char *p = strchr(sig->i, '@');
+ size_t ilen, dlen;
+ if (!p)
+ return DKIM_EXPL_SIG_SYNTAX;
+ p++;
+ ilen = strlen(p);
+ dlen = strlen(sig->d);
+ if (!(dlen <= ilen &&
+ mu_c_strcasecmp(sig->d, p + ilen - dlen) == 0 &&
+ sig->d[ilen - dlen - 1] == '.'))
+ return DKIM_EXPL_DOMAIN_MISMATCH;
+ }
+
+ if (!dkim_header_list_match(sig->h, MU_HEADER_FROM))
+ return DKIM_EXPL_FROM;
+
+ if (sig->x && time(NULL) > sig->x)
+ return DKIM_EXPL_EXPIRED;
+
+ return DKIM_EXPL_OK;
+}
+
+static int
+dnsrec_parse(char *rec, mu_assoc_t *pa)
+{
+ mu_assoc_t a;
+ struct mu_wordsplit ws;
+ int result;
+
+ if (mu_assoc_create (&a, 0))
+ mu_alloc_die ();
+ mu_assoc_set_destroy_item (a, mu_list_free_item);
+
+ ws.ws_delim = ";";
+ if (mu_wordsplit(rec,
+ &ws,
+ MU_WRDSF_DELIM |
+ MU_WRDSF_NOVAR |
+ MU_WRDSF_WS |
+ WRDSF_NOCMD)) {
+ result = 1;
+ } else {
+ size_t i;
+
+ for (i = 0; i < ws.ws_wordc; i++) {
+ char *p = strchr(ws.ws_wordv[i], '=');
+ char **slot;
+ int rc;
+
+ if (!p) {
+ result = 1;
+ break;
+ }
+ *p++ = 0;
+
+ rc = mu_assoc_install_ref(a, ws.ws_wordv[i], &slot);
+ if (rc == ENOMEM)
+ mu_alloc_die ();
+ else if (rc) {
+ result = 1;
+ break;
+ } else
+ result = 0;
+ *slot = mu_strdup(p);
+ }
+ }
+
+ mu_wordsplit_free(&ws);
+ if (result)
+ mu_assoc_destroy (&a);
+ else
+ *pa = a;
+ return result;
+}
+
+static int
+pubkey_validate(mu_assoc_t a, struct dkim_signature const *sig)
+{
+ char *s;
+ if ((s = mu_assoc_get(a, "p")) == NULL)
+ return DKIM_EXPL_KEY_SYNTAX;
+
+ if (s[0] == 0)
+ return DKIM_EXPL_KEY_REVOKED;
+
+ if ((s = mu_assoc_get(a, "h")) != NULL &&
+ !dkim_header_list_match(s, sig->a))
+ return DKIM_EXPL_BAD_ALGORITHM;
+ return DKIM_EXPL_OK;
+}
+
+static int
+dkim_sig_key_verify(mu_message_t msg, struct dkim_signature *sig,
+ char const *sigstr,
+ struct rsa_public_key *pub)
+{
+ struct sha256_ctx ctx;
+ mpz_t bs;
+ int rc;
+ struct nettle_buffer buffer;
+ size_t length;
+
+ sha256_init(&ctx);
+ switch (dkim_hash(msg, sig, sigstr, &ctx)) {
+ case DKIM_HASH_OK:
+ break;
+
+ case DKIM_HASH_DIFF:
+ return DKIM_EXPL_BAD_BODY;
+
+ case DKIM_HASH_ERR:
+ return DKIM_EXPL_INTERNAL_ERROR;
+ }
+
+ length = strlen((char*)sig->b);
+ nettle_buffer_init_realloc(&buffer, NULL, nettle_xrealloc);
+ nettle_buffer_write(&buffer, length, sig->b);
+ if (decode_base64(&buffer, 0, &length) != READ_PEM_OK)
+ return DKIM_EXPL_BAD_BASE64;
+
+ nettle_mpz_init_set_str_256_u(bs, length, buffer.contents);
+ rc = rsa_sha256_verify (pub, &ctx, bs);
+ nettle_buffer_clear(&buffer);
+ mpz_clear(bs);
+
+ return rc ? DKIM_EXPL_OK : DKIM_EXPL_BAD_SIG;
+}
+
+static int
+dkim_sig_verify(mu_message_t msg, struct dkim_signature *sig, char *sig_str)
+{
+ char **dnsrec;
+ int i;
+ int result;
+
+ /* Get the DKIM DNS record */
+ switch (dkim_lookup(sig->d, sig->s, &dnsrec)) {
+ case dns_success:
+ break;
+
+ case dns_not_found:
+ return DKIM_EXPL_DNS_NOTFOUND;
+
+ default:
+ return DKIM_EXPL_DNS_UNAVAIL;
+ }
+
+ for (i = 0; dnsrec[i]; i++) {
+ mu_assoc_t a;
+ struct rsa_public_key pub;
+ int rc;
+
+ if (dnsrec_parse(dnsrec[i], &a))
+ continue;
+
+ if ((rc = pubkey_validate(a, sig)) != DKIM_EXPL_OK) {
+ result = rc;
+ } else if (pubkey_from_base64(&pub, mu_assoc_get(a, "p"))
+ != READ_PEM_OK) {
+ result = DKIM_EXPL_KEY_SYNTAX;
+ } else {
+ result = dkim_sig_key_verify(msg, sig, sig_str, &pub);
+ rsa_public_key_clear(&pub);
+ }
+ mu_assoc_destroy(&a);
+
+ if (result == DKIM_EXPL_OK)
+ break;
+ }
+
+ for (i = 0; dnsrec[i]; i++)
+ free(dnsrec[i]);
+ free(dnsrec);
+
+ return result;
+}
+
+int
+mfd_dkim_verify(mu_message_t msg)
+{
+ mu_header_t hdr;
+ int rc;
+ int result = DKIM_EXPL_NO_SIG;
+ size_t i;
+
+ /* Get the DKIM-Signature header from the message */
+ rc = mu_message_get_header(msg, &hdr);
+ if (rc) {
+ mu_diag_funcall(MU_DIAG_ERROR,
+ "mu_message_get_header", NULL, rc);
+ return DKIM_EXPL_INTERNAL_ERROR;
+ }
+
+ for (i = 1; result != DKIM_EXPL_OK; i++) {
+ struct dkim_signature sig;
+ char *sig_str;
+
+ rc = mu_header_aget_value_unfold_n(hdr,
+ DKIM_SIGNATURE_HEADER,
+ i,
+ &sig_str);
+ if (rc == MU_ERR_NOENT)
+ break;
+ else if (rc) {
+ mu_diag_funcall(MU_DIAG_ERROR,
+ "mu_header_aget_value_unfold",
+ NULL, rc);
+ result = DKIM_EXPL_INTERNAL_ERROR;
+ break;
+ }
+
+ /* Parse the DKIM signature */
+ if (dkim_signature_parse(sig_str, &sig)) {
+ result = DKIM_EXPL_SIG_SYNTAX;
+ } else {
+ /* Validate the signature */
+ result = dkim_sig_validate(&sig);
+ if (result == DKIM_EXPL_OK)
+ result = dkim_sig_verify(msg, &sig, sig_str);
+ dkim_signature_free(&sig);
+ }
+ free(sig_str);
+ }
+ return result;
+}
+
diff --git a/src/dkim.h b/src/dkim.h
index 1d0f97e5..aaa83a93 100644
--- a/src/dkim.h
+++ b/src/dkim.h
@@ -16,6 +16,9 @@
/* DKIM implementation defines */
+/* Verification result codes are defined in status.h */
+#include <mflib/status.h>
+
/* Canonicalization types. */
enum {
DKIM_CANON_ERR = -1,
@@ -23,12 +26,18 @@ enum {
DKIM_CANON_RELAXED
};
+#define DKIM_VERSION "1"
+#define DKIM_SIGNATURE_HEADER "DKIM-Signature"
+#define DKIM_QUERY_METHOD "dns/txt"
+#define DKIM_ALGORITHM "rsa-sha256"
+
/*
* Structure governing creation of a DKIM signature.
* Most members are named after the DKIM-Signature tags.
* See RFC 6376, 3.5. "The DKIM-Signature Header Field" (page 17).
*/
struct dkim_signature {
+ char *a;
uint8_t *b;
uint8_t *bh;
int canon[2];
@@ -36,8 +45,10 @@ struct dkim_signature {
char *s;
char *h;
char *i;
+ char *q;
time_t t;
time_t x;
+ char *v;
};
/* Convert canonicalization type to a DKIM_CANON_ constant. */
@@ -60,3 +71,40 @@ int dkim_canonicalizer_create(mu_stream_t *pstream,
*/
int mfd_dkim_sign(mu_message_t msg, struct dkim_signature *sig,
char *priv_key, char **ret_sighdr);
+
+int dkim_header_list_match(char const *h_list, char const *h);
+
+/* Explanatory error codes */
+enum {
+ DKIM_EXPL_OK,
+ DKIM_EXPL_NO_SIG,
+ DKIM_EXPL_INTERNAL_ERROR,
+ DKIM_EXPL_SIG_SYNTAX,
+ DKIM_EXPL_SIG_MISS,
+ DKIM_EXPL_DOMAIN_MISMATCH,
+ DKIM_EXPL_BAD_VERSION,
+ DKIM_EXPL_BAD_ALGORITHM,
+ DKIM_EXPL_BAD_QUERY,
+ DKIM_EXPL_FROM,
+ DKIM_EXPL_EXPIRED,
+ DKIM_EXPL_DNS_UNAVAIL,
+ DKIM_EXPL_DNS_NOTFOUND,
+ DKIM_EXPL_KEY_SYNTAX,
+ DKIM_EXPL_KEY_REVOKED,
+ DKIM_EXPL_BAD_BODY,
+ DKIM_EXPL_BAD_BASE64,
+ DKIM_EXPL_BAD_SIG,
+};
+
+/* Verification error codes */
+enum {
+ DKIM_VERIFY_OK = _MFL_DKIM_VERIFY_OK,
+ DKIM_VERIFY_PERMFAIL = _MFL_DKIM_VERIFY_PERMFAIL,
+ DKIM_VERIFY_TEMPFAIL = _MFL_DKIM_VERIFY_TEMPFAIL,
+};
+
+int mfd_dkim_verify(mu_message_t msg);
+
+extern char const *dkim_explanation_str[];
+extern int dkim_result_trans[];
+

Return to:

Send suggestions and report system problems to the System administrator.