DKIM: support multiple hash methods
[exim.git] / src / src / pdkim / pdkim.c
index d289ec7a336e60e9e8322778c34f60f046bf8031..bef6b6a6999c3db0f065a09e9618d06ce8d9f92d 100644 (file)
 
 #include "crypt_ver.h"
 
-#ifdef RSA_OPENSSL
+#ifdef SIGN_OPENSSL
 # include <openssl/rsa.h>
 # include <openssl/ssl.h>
 # include <openssl/err.h>
-#elif defined(RSA_GNUTLS)
+#elif defined(SIGN_GNUTLS)
 # include <gnutls/gnutls.h>
 # include <gnutls/x509.h>
 #endif
 
 #include "pdkim.h"
-#include "rsa.h"
+#include "signing.h"
 
 #define PDKIM_SIGNATURE_VERSION     "1"
 #define PDKIM_PUB_RECORD_VERSION    US "DKIM1"
@@ -73,24 +73,24 @@ const uschar * pdkim_querymethods[] = {
   US"dns/txt",
   NULL
 };
-const uschar * pdkim_algos[] = {
-  US"rsa-sha256",
-  US"rsa-sha1",
-  NULL
-};
 const uschar * pdkim_canons[] = {
   US"simple",
   US"relaxed",
   NULL
 };
-const uschar * pdkim_hashes[] = {
-  US"sha256",
-  US"sha1",
-  NULL
+
+typedef struct {
+  const uschar * dkim_hashname;
+  hashmethod    exim_hashmethod;
+} pdkim_hashtype;
+static const pdkim_hashtype pdkim_hashes[] = {
+  { US"sha1",   HASH_SHA1 },
+  { US"sha256", HASH_SHA2_256 },
+  { US"sha512", HASH_SHA2_512 }
 };
+
 const uschar * pdkim_keytypes[] = {
-  US"rsa",
-  NULL
+  US"rsa"
 };
 
 typedef struct pdkim_combined_canon_entry {
@@ -111,6 +111,17 @@ pdkim_combined_canon_entry pdkim_combined_canons[] = {
 
 
 /* -------------------------------------------------------------------------- */
+uschar *
+dkim_sig_to_a_tag(pdkim_signature * sig)
+{
+if (  sig->keytype < 0  || sig->keytype > nelem(pdkim_keytypes)
+   || sig->hashtype < 0 || sig->hashtype > nelem(pdkim_hashes))
+  return US"err";
+return string_sprintf("%s-%s",
+  pdkim_keytypes[sig->keytype], pdkim_hashes[sig->hashtype].dkim_hashname);
+}
+
+
 
 const char *
 pdkim_verify_status_str(int status)
@@ -289,7 +300,7 @@ found:
 /* 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;
@@ -300,8 +311,8 @@ uschar * q = relaxed;
 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 == ' ')
     {
@@ -313,8 +324,8 @@ for (p = header; *p; p++)
   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
@@ -327,7 +338,7 @@ for (p = header; *p; p++)
 
 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;
 }
@@ -336,10 +347,10 @@ 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++;
@@ -362,11 +373,11 @@ return initial_pos;
 /* -------------------------------------------------------------------------- */
 
 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';
@@ -394,7 +405,7 @@ return n;
 /* -------------------------------------------------------------------------- */
 
 static void
-pdkim_decode_base64(uschar *str, blob * b)
+pdkim_decode_base64(const uschar * str, blob * b)
 {
 int dlen;
 dlen = b64decode(str, &b->data);
@@ -431,8 +442,9 @@ memset(sig, 0, sizeof(pdkim_signature));
 sig->bodylength = -1;
 
 /* Set so invalid/missing data error display is accurate */
-sig->algo = -1;
 sig->version = 0;
+sig->keytype = -1;
+sig->hashtype = -1;
 
 q = sig->rawsig_no_b_val = store_get(Ustrlen(raw_hdr)+1);
 
@@ -505,13 +517,18 @@ for (p = raw_hdr; ; p++)
              Ustrcmp(cur_val, PDKIM_SIGNATURE_VERSION) == 0 ? 1 : -1;
            break;
          case 'a':
-           for (i = 0; pdkim_algos[i]; i++)
-             if (Ustrcmp(cur_val, pdkim_algos[i]) == 0)
-               {
-               sig->algo = i;
-               break;
-               }
+           {
+           uschar * s = Ustrchr(cur_val, '-');
+
+           for(i = 0; i < nelem(pdkim_keytypes); i++)
+             if (Ustrncmp(cur_val, pdkim_keytypes[i], s - cur_val) == 0)
+               { sig->keytype = i; break; }
+           for (++s, i = 0; i < nelem(pdkim_hashes); i++)
+             if (Ustrcmp(s, pdkim_hashes[i].dkim_hashname) == 0)
+               { sig->hashtype = i; break; }
            break;
+           }
+
          case 'c':
            for (i = 0; pdkim_combined_canons[i].str; i++)
              if (Ustrcmp(cur_val, pdkim_combined_canons[i].str) == 0)
@@ -583,10 +600,12 @@ DEBUG(D_acl)
          "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
   }
 
+/*XXX hash method: extend for sha512 */
 if (!exim_sha_init(&sig->body_hash_ctx,
-              sig->algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256))
+              pdkim_hashes[sig->hashtype].exim_hashmethod))
   {
-  DEBUG(D_acl) debug_printf("PDKIM: hash init internal error\n");
+  DEBUG(D_acl)
+    debug_printf("PDKIM: hash init error, possibly nonhandled hashtype\n");
   return NULL;
   }
 return sig;
@@ -598,84 +617,38 @@ 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)))
+  {
+  const uschar * val;
+
+  if ((val = Ustrchr(ele, '=')))
     {
-    uschar c = *p;
+    int taglen = val++ - ele;
 
-    /* Ignore FWS */
-    if (c != '\r' && c != '\n') switch (where)
+    DEBUG(D_acl) debug_printf(" %.*s=%s\n", taglen, ele, val);
+    switch (ele[0])
       {
-      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*/
-
-      case PDKIM_HDR_TAG:
-       if (c >= 'a' && c <= 'z')
-         cur_tag = string_catn(cur_tag, &ts, &tl, p, 1);
-
-       if (c == '=')
-         {
-         cur_tag[tl] = '\0';
-         where = PDKIM_HDR_VALUE;
-         }
-       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':
-               pub->hashes = string_copy(cur_val); break;
-             case 'k':
-               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;
+      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;
-             }
-           }
-         tl = 0;
-         vl = 0;
-         where = PDKIM_HDR_LIMBO;
-         }
-       else
-         cur_val = string_catn(cur_val, &vs, &vl, p, 1);
-       break;
+      default:  DEBUG(D_acl) debug_printf(" Unknown tag encountered\n"); break;
       }
-
-    if (c == '\0') break;
     }
+  }
 
 /* Set fallback defaults */
 if (!pub->version    ) pub->version     = string_copy(PDKIM_PUB_RECORD_VERSION);
@@ -685,11 +658,11 @@ else if (Ustrcmp(pub->version, PDKIM_PUB_RECORD_VERSION) != 0)
   return NULL;
   }
 
-if (!pub->granularity) pub->granularity = string_copy(US"*");
+if (!pub->granularity) pub->granularity = US"*";
 /*
-if (!pub->keytype    ) pub->keytype     = string_copy(US"rsa");
+if (!pub->keytype    ) pub->keytype     = US"rsa";
 */
-if (!pub->srvtype    ) pub->srvtype     = string_copy(US"*");
+if (!pub->srvtype    ) pub->srvtype     = US"*";
 
 /* p= is required */
 if (pub->key.data)
@@ -1231,7 +1204,7 @@ col = hdr_len;
 
 /* Required and static bits */
 hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"a=",
-                   pdkim_algos[sig->algo]);
+                   dkim_sig_to_a_tag(sig));
 hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"q=",
                    pdkim_querymethods[sig->querymethod]);
 hdr = pdkim_headcat(&col, hdr, &hdr_size, &hdr_len, US";", US"c=",
@@ -1299,11 +1272,23 @@ if (sig->bodylength >= 0)
   }
 
 /* Preliminary or final version? */
-base64_b = final ? pdkim_encode_base64(&sig->sighash) : 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;
@@ -1366,7 +1351,7 @@ DEBUG(D_acl) debug_printf(
       "PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
 
 /* Import public key */
-if ((*errstr = exim_rsa_verify_init(&p->key, vctx)))
+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;
@@ -1404,7 +1389,6 @@ pdkim_finish_bodyhash(ctx);
 
 while (sig)
   {
-  BOOL is_sha1 = sig->algo == PDKIM_ALGO_RSA_SHA1;
   hctx hhash_ctx;
   uschar * sig_hdr = US"";
   blob hhash;
@@ -1414,9 +1398,10 @@ while (sig)
   hdata.data = NULL;
   hdata.len = 0;
 
-  if (!exim_sha_init(&hhash_ctx, is_sha1 ? HASH_SHA1 : HASH_SHA256))
+  if (!exim_sha_init(&hhash_ctx, pdkim_hashes[sig->hashtype].exim_hashmethod))
     {
-    DEBUG(D_acl) debug_printf("PDKIM: hask setup internal error\n");
+    DEBUG(D_acl)
+      debug_printf("PDKIM: hash setup error, possibly nonhandled hashtype\n");
     break;
     }
 
@@ -1449,14 +1434,14 @@ while (sig)
                        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));
        }
@@ -1510,7 +1495,7 @@ while (sig)
            /* 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)
+             ? pdkim_relax_header(hdrs->value, TRUE)
              : string_copy(CUS hdrs->value);
 
            /* Feed header to the hash algorithm */
@@ -1534,7 +1519,7 @@ while (sig)
 
   /* 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)
     {
@@ -1556,16 +1541,18 @@ while (sig)
     }
 
   /* Remember headers block for signing (when the library cannot do incremental)  */
+/*XXX is this assuing algo == RSA? */
   if (ctx->flags & PDKIM_MODE_SIGN)
-    (void) exim_rsa_data_append(&hdata, &hdata_alloc, US sig_hdr);
+    (void) exim_dkim_data_append(&hdata, &hdata_alloc, US sig_hdr);
 
   /* SIGNING ---------------------------------------------------------------- */
   if (ctx->flags & PDKIM_MODE_SIGN)
     {
     es_ctx sctx;
 
-    /* Import private key */
-    if ((*err = exim_rsa_signing_init(US sig->rsa_privkey, &sctx)))
+    /* Import private key, including the keytype */
+/*XXX extend for non-RSA algos */
+    if ((*err = exim_dkim_signing_init(US sig->privkey, &sctx)))
       {
       DEBUG(D_acl) debug_printf("signing_init: %s\n", *err);
       return PDKIM_ERR_RSA_PRIVKEY;
@@ -1575,11 +1562,14 @@ while (sig)
     calculated, with GnuTLS we have to sign an entire block of headers
     (due to available interfaces) and it recalculates the hash internally. */
 
-#if defined(RSA_OPENSSL) || defined(RSA_GCRYPT)
+#if defined(SIGN_OPENSSL) || defined(SIGN_GCRYPT)
     hdata = hhash;
 #endif
 
-    if ((*err = exim_rsa_sign(&sctx, is_sha1, &hdata, &sig->sighash)))
+/*XXX extend for non-RSA algos */
+    if ((*err = exim_dkim_sign(&sctx,
+                 pdkim_hashes[sig->hashtype].exim_hashmethod,
+                 &hdata, &sig->sighash)))
       {
       DEBUG(D_acl) debug_printf("signing: %s\n", *err);
       return PDKIM_ERR_RSA_SIGNING;
@@ -1605,7 +1595,8 @@ while (sig)
         && sig->headernames   && *sig->headernames
         && sig->bodyhash.data
         && sig->sighash.data
-        && sig->algo > -1
+        && sig->keytype >= 0
+        && sig->hashtype >= 0
         && sig->version
        ) )
       {
@@ -1641,11 +1632,13 @@ while (sig)
       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 (Ustrcmp(ele, pdkim_hashes[sig->hashtype].dkim_hashname) == 0) break;
       if (!ele)
        {
-       DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%s\n",
-                               sig->pubkey->hashes, pdkim_algos[sig->algo]);
+       DEBUG(D_acl) debug_printf("pubkey h=%s vs. sig a=%s_%s\n",
+         sig->pubkey->hashes,
+         pdkim_keytypes[sig->keytype],
+         pdkim_hashes[sig->hashtype].dkim_hashname);
        sig->verify_status =      PDKIM_VERIFY_FAIL;
        sig->verify_ext_status =  PDKIM_VERIFY_FAIL_SIG_ALGO_MISMATCH;
        goto NEXT_VERIFY;
@@ -1653,7 +1646,10 @@ while (sig)
       }
 
     /* Check the signature */
-    if ((*err = exim_rsa_verify(&vctx, is_sha1, &hhash, &sig->sighash)))
+/*XXX needs extension for non-RSA */
+    if ((*err = exim_dkim_verify(&vctx,
+                 pdkim_hashes[sig->hashtype].exim_hashmethod,
+                 &hhash, &sig->sighash)))
       {
       DEBUG(D_acl) debug_printf("headers verify: %s\n", *err);
       sig->verify_status =      PDKIM_VERIFY_FAIL;
@@ -1711,15 +1707,18 @@ return ctx;
 
 /* -------------------------------------------------------------------------- */
 
+/*XXX ? needs extension to cover non-RSA algo?  */
+
 DLLEXPORT pdkim_ctx *
-pdkim_init_sign(char * domain, char * selector, char * rsa_privkey, int algo,
-  BOOL dot_stuffed, int(*dns_txt_callback)(char *, char *),
+pdkim_init_sign(uschar * domain, uschar * selector, uschar * privkey,
+  uschar * hashname, BOOL dot_stuffed, int(*dns_txt_callback)(char *, char *),
   const uschar ** errstr)
 {
+int hashtype;
 pdkim_ctx * ctx;
 pdkim_signature * sig;
 
-if (!domain || !selector || !rsa_privkey)
+if (!domain || !selector || !privkey)
   return NULL;
 
 ctx = store_get(sizeof(pdkim_ctx) + PDKIM_MAX_BODY_LINE_LEN + sizeof(pdkim_signature));
@@ -1738,13 +1737,23 @@ ctx->sig = sig;
 
 sig->domain = string_copy(US domain);
 sig->selector = string_copy(US selector);
-sig->rsa_privkey = string_copy(US rsa_privkey);
-sig->algo = algo;
+sig->privkey = string_copy(US privkey);
+/*XXX no keytype yet; comes from privkey */
 
-if (!exim_sha_init(&sig->body_hash_ctx,
-              algo == PDKIM_ALGO_RSA_SHA1 ? HASH_SHA1 : HASH_SHA256))
+for (hashtype = 0; hashtype < nelem(pdkim_hashes); hashtype++)
+  if (Ustrcmp(hashname, pdkim_hashes[hashtype].dkim_hashname) == 0)
+  { sig->hashtype = hashtype; break; }
+if (hashtype >= nelem(pdkim_hashes))
   {
-  DEBUG(D_acl) debug_printf("PDKIM: hash setup internal error\n");
+  DEBUG(D_acl)
+    debug_printf("PDKIM: unrecognised hashname '%s'\n", hashname);
+  return NULL;
+  }
+
+if (!exim_sha_init(&sig->body_hash_ctx, pdkim_hashes[hashtype].exim_hashmethod))
+  {
+  DEBUG(D_acl)
+    debug_printf("PDKIM: hash setup error, possibly nonhandled hashtype\n");
   return NULL;
   }
 
@@ -1795,7 +1804,7 @@ return PDKIM_OK;
 void
 pdkim_init(void)
 {
-exim_rsa_init();
+exim_dkim_init();
 }