diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2020-07-06 14:24:54 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2020-07-06 16:07:53 +0300 |
commit | e66164eb05b03be03ccf6187f5b09ca8aba483cf (patch) | |
tree | 88050c4c2ac64472784e175977575e22c4500a67 /src | |
parent | 2d56241f89eae839959e538cede756539adba89e (diff) | |
download | mailfromd-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.bi | 42 | ||||
-rw-r--r-- | src/dkim.c | 719 | ||||
-rw-r--r-- | src/dkim.h | 48 |
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 @@ -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; +} + @@ -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[]; + |