From f1b71c9fe7dbb4886588a036399cf5ebe16b7c47 Mon Sep 17 00:00:00 2001
From: Jannis Hoffmann <jannis@fehcom.de>
Date: Tue, 9 Jul 2024 11:44:11 +0200
Subject: removed top level directory

---
 src/dkimverify.cpp | 1443 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1443 insertions(+)
 create mode 100644 src/dkimverify.cpp

(limited to 'src/dkimverify.cpp')

diff --git a/src/dkimverify.cpp b/src/dkimverify.cpp
new file mode 100644
index 0000000..c9f1003
--- /dev/null
+++ b/src/dkimverify.cpp
@@ -0,0 +1,1443 @@
+/*****************************************************************************
+*
+*  Licensed under the Apache License, Version 2.0 (the "License"); 
+*  you may not use this file except in compliance with the License. 
+*  You may obtain a copy of the License at 
+*
+*      http://www.apache.org/licenses/LICENSE-2.0 
+*
+*  This code incorporates intellectual property owned by Yahoo! and licensed 
+*  pursuant to the Yahoo! DomainKeys Patent License Agreement.
+*
+*  Unless required by applicable law or agreed to in writing, software 
+*  distributed under the License is distributed on an "AS IS" BASIS, 
+*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+*  See the License for the specific language governing permissions and 
+*  limitations under the License.
+*
+*  Changes done by ¢feh@fehcom.de obeying the above license
+*
+*****************************************************************************/
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <vector>
+#include <algorithm>
+#include "dkim.h"
+#include "dkimverify.h"
+#include "dnsgettxt.h"
+extern "C" { 
+#include "dns.h"
+#include "stralloc.h"
+}
+
+/*****************************************************************************
+*
+* Verifying DKIM Ed25519 signatures:
+*
+* The received DKIM header includes two cryptographic relevant informations:
+*
+* a) The 'body hash' => bh=[sha1|sha256]
+* b) The signature   =>  b=[RSA-SHA1|RSA-SHA256|PureEd25519]
+*
+* Several DKIM headers (=signatures) may be present in the email. 
+* Here, it is limited to max. Shall we really evaluate all?
+*
+* Caution: Using hybrid signatures, calling the destructor will core dump 
+*          given EVP_MD_CTX_free() upon the next call of EVP_DigestInit.
+*          Using the destructor with EVP_MD_CTX_reset() however works.
+*
+*****************************************************************************/
+
+#define _strnicmp strncasecmp
+#define _stricmp strcasecmp
+#define MAX_SIGNATURES  10  // maximum number of DKIM signatures to process/message
+#define FDLOG stderr  /* writing to another FD requires a method */
+
+string SigHdr;
+size_t m_SigHdr;
+
+extern "C" int stralloc_copys(stralloc *,char const *);
+
+int dig_ascii(char *digascii,unsigned const char *digest,const int len)
+{
+  static const char hextab[] = "0123456789abcdef";
+  int j;
+
+  for (j = 0; j < len; j++) {
+    digascii[2 * j] = hextab[(unsigned char)digest[j] >> 4];
+    digascii[2 * j + 1] = hextab[(unsigned char)digest[j] & 0x0f];
+  }
+  digascii[2 * len] = '\0';
+
+  return (2 * j);  // 2*len
+}
+
+
+int _DNSGetTXT(const char *szFQDN,char *Buffer,int nBufLen)
+{
+   stralloc out = {0};
+   stralloc sa = {0};
+   Buffer[0] = '\0';  // need to be initialized
+   
+   if (!stralloc_copys(&sa,szFQDN)) return -1;
+   
+   DNS_INIT
+   
+   switch (dns_txt(&out,&sa)) {
+     case -1: return -1;
+     case  0: return 0;
+   } 
+
+   if (nBufLen < out.len) 
+     return -2;
+
+   if (!stralloc_0(&out)) return -1;
+   memcpy(Buffer,out.s,out.len);  // Return-by-value; sigh
+
+   return out.len;
+}
+
+int _DKIM_ReportResult(const char* ResFile,const char* result,const char* reason)
+{
+  int len = 0;
+
+  FILE* out = fopen(ResFile,"wb+");
+  if (out == NULL) return -1;
+
+  if (result) {
+    len = strlen(result);
+    fwrite(result,1,len,out);
+    fwrite("\r",1,1,out);
+  }
+
+  if (reason) {
+    fwrite(reason,1,strlen(reason),out);
+    fwrite("\r",1,1,out);
+  }
+  fclose(out);
+
+  return len;
+}
+
+const char* DKIM_ErrorResult(const int res)
+{
+  const char* errormsg = "";
+
+  switch (res) {
+    case DKIM_FAIL:
+      errormsg = " (verify error: message is suspicious)";
+      break;
+    case DKIM_BAD_SYNTAX:
+      errormsg = " (signature error: could not parse or has bad tags/values)";
+      break;
+    case DKIM_SIGNATURE_BAD:
+      errormsg = " (signature error: RSA/ED25519 verify failed)";
+      break;
+    case DKIM_SIGNATURE_BAD_BUT_TESTING:
+      errormsg = " (signature error: RSA/ED25519 verify failed but testing)";
+      break;
+    case DKIM_SIGNATURE_EXPIRED:
+      errormsg = " (signature error: signature x= value expired)";
+      break;
+    case DKIM_SELECTOR_INVALID:
+      errormsg = " (signature error: selector doesn't parse or contains invalid values)";
+      break;
+    case DKIM_SELECTOR_GRANULARITY_MISMATCH:
+      errormsg = " (signature error: selector g= doesn't match i=)";
+      break;
+    case DKIM_SELECTOR_KEY_REVOKED:
+      errormsg = " (signature error: revoked p= empty)";
+      break;
+    case DKIM_SELECTOR_DOMAIN_NAME_TOO_LONG:
+      errormsg = " (dns error: selector domain name too long to request)";
+      break;
+    case DKIM_SELECTOR_DNS_TEMP_FAILURE:
+      errormsg = " (dns error: temporary dns failure requesting selector)";
+      break;
+    case DKIM_SELECTOR_DNS_PERM_FAILURE:
+      errormsg = " (dns error: permanent dns failure requesting selector)";
+      break;
+    case DKIM_SELECTOR_PUBLIC_KEY_INVALID:
+      errormsg = " (signature error: selector p= value invalid or wrong format)";
+      break;
+    case DKIM_NO_SIGNATURES:
+      errormsg = " (process error: no signatures)";
+      break;
+    case DKIM_NO_VALID_SIGNATURES:
+      errormsg = " (process error: no valid signatures)";
+      break;
+    case DKIM_BODY_HASH_MISMATCH:
+      errormsg = " (signature verify error: message body does not hash to bh= value)";
+      break;
+    case DKIM_SELECTOR_ALGORITHM_MISMATCH:
+      errormsg = " (signature error: selector h= doesn't match signature a=)";
+      break;
+    case DKIM_STAT_INCOMPAT:
+      errormsg = " (signature error: incompatible v= value)";
+      break;
+    case DKIM_UNSIGNED_FROM:
+      errormsg = " (signature error: not all message's From headers in signature)";
+      break;
+    case DKIM_OUT_OF_MEMORY:
+      errormsg = " (internal error: memory allocation failed)";
+      break;
+    case DKIM_INVALID_CONTEXT:
+      errormsg = " (internal error: DKIMContext structure invalid for this operation)";
+      break;
+    case DKIM_NO_SENDER:
+      errormsg = " (signing error: Could not find From: or Sender: header in message)";
+      break;
+    case DKIM_BAD_PRIVATE_KEY:
+      errormsg = " (signing error: Could not parse private key)";
+      break;
+    case DKIM_BUFFER_TOO_SMALL:
+      errormsg = " (signing error: Buffer passed in is not large enough)";
+      break;
+  }
+
+  return errormsg;
+}
+
+SignatureInfo::SignatureInfo(bool s)
+{
+  VerifiedBodyCount = 0;
+  UnverifiedBodyCount = 0;
+
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+  EVP_MD_CTX_init(&m_Hdr_ctx);
+  EVP_MD_CTX_init(&m_Bdy_ctx);
+#else
+  m_Hdr_ctx = EVP_MD_CTX_new();
+  m_Bdy_ctx = EVP_MD_CTX_new();
+#endif
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L)
+  m_Msg_ctx = EVP_MD_CTX_new();
+#endif
+  m_pSelector = NULL;
+  Status = DKIM_SUCCESS;
+  m_nHash = 0;
+  EmptyLineCount = 0;
+  m_SaveCanonicalizedData = s;
+}
+
+SignatureInfo::~SignatureInfo()
+{
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+  EVP_MD_CTX_cleanup(&m_Hdr_ctx);
+  EVP_MD_CTX_cleanup(&m_Bdy_ctx);
+#else
+  /** FIXME: No free but reset ! **/
+  EVP_MD_CTX_reset(m_Hdr_ctx);  
+  EVP_MD_CTX_reset(m_Bdy_ctx);
+#endif
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L)
+  EVP_MD_CTX_reset(m_Msg_ctx);
+#endif
+}
+
+inline bool isswsp(char ch)
+{
+  return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Parse a DKIM tag-list.  Returns true for success
+//
+////////////////////////////////////////////////////////////////////////////////
+bool ParseTagValueList(char *tagvaluelist,const char *wanted[],char *values[])
+{
+  char *s = tagvaluelist;
+
+  for (;;) {
+    // skip whitespace
+    while (isswsp(*s))
+      s++;
+
+    // if at the end of the string, return success.  Note: this allows a list with no entries
+    if (*s == '\0')
+      return true;
+
+    // get tag name
+    if (!isalpha(*s))
+      return false;
+
+    char *tag = s;
+    do {
+      s++;
+    } while (isalnum(*s) || *s == '-');
+
+    char *endtag = s;
+
+    // skip whitespace before equals
+    while (isswsp(*s))
+      s++;
+
+    // next character must be equals
+    if (*s != '=')
+      return false;
+    s++;
+
+    // null-terminate tag name
+    *endtag = '\0';
+
+    // skip whitespace after equals
+    while (isswsp(*s))
+      s++;
+
+    // get tag value
+    char *value = s;
+
+    while (*s != ';' && ((*s == '\t' || *s == '\r' || *s == '\n') || (*s >= ' ' && *s <= '~')))
+      s++;
+
+    char *e = s;
+
+    // make sure the next character is the null terminator (which means we're done) or a semicolon (not done)
+    bool done = false;
+    if (*s == '\0')
+      done = true;
+    else {
+      if (*s != ';')
+        return false;
+      s++;
+    }
+
+    // skip backwards past any trailing whitespace
+    while (e > value && isswsp(e[-1]))
+      e--;
+
+    // null-terminate tag value
+    *e = '\0';
+
+    // check to see if we want this tag
+    for (unsigned i = 0; wanted[i] != NULL; i++) {
+      if (strcmp(wanted[i],tag) == 0) {
+        // return failure if we already have a value for this tag (duplicates not allowed)
+        if (values[i] != NULL)
+          return false;
+        values[i] = value;
+        break;
+      }
+    }
+
+    if (done)
+      return true;
+  }
+}
+////////////////////////////////////////////////////////////////////////////////
+//
+// Convert hex char to value (0-15)
+//
+////////////////////////////////////////////////////////////////////////////////
+char Tohex(char ch)
+{
+  if (ch >= '0' && ch <= '9')
+    return (ch - '0');
+  else if (ch >= 'A' && ch <= 'F')
+    return (ch - 'A' + 10);
+  else if (ch >= 'a' && ch <= 'f')
+    return (ch - 'a' + 10);
+  else {
+    assert(0);
+    return 0;
+  }
+}
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Decode quoted printable string in-place
+//
+////////////////////////////////////////////////////////////////////////////////
+void DecodeQuotedPrintable(char* ptr)
+{
+  char *s = ptr;
+  while (*s != '\0' && *s != '=')
+    s++;
+
+  if (*s == '\0')
+    return;
+
+  char *d = s;
+  do {
+    if (*s == '=' && isxdigit(s[1]) && isxdigit(s[2])) {
+      *d++ = (Tohex(s[1]) << 4) | Tohex(s[2]);
+      s += 3;
+    } else {
+      *d++ = *s++;
+    }
+  } while (*s != '\0');
+  *d = '\0';
+}
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Decode base64 string in-place, returns number of bytes output
+//
+////////////////////////////////////////////////////////////////////////////////
+unsigned DecodeBase64(char *ptr)
+{
+  static const char base64_table[256] = {
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
+    -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
+    -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
+
+  unsigned char* s = (unsigned char* )ptr;
+  unsigned char* d = (unsigned char* )ptr;
+  unsigned b64accum = 0;
+  unsigned char b64shift = 0;
+
+  while (*s != '\0') {
+    unsigned char value = base64_table[*s++];
+    if ((signed char) value >= 0) {
+      b64accum = (b64accum << 6) | value;
+      b64shift += 6;
+      if (b64shift >= 8) {
+        b64shift -= 8;
+        *d++ = (b64accum >> b64shift);
+      }
+    }
+  }
+
+  return (char* )d-ptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Match a string with a pattern (used for g= value)
+// Supports a single, optional "*" wildcard character.
+//
+////////////////////////////////////////////////////////////////////////////////
+bool WildcardMatch(const char *p, const char *s)
+{
+  // special case: An empty "g=" value never matches any addresses
+  if (*p == '\0')
+    return false;
+
+  const char* wildcard = strchr(p,'*');
+  if (wildcard == NULL) {
+    return strcmp(s, p) == 0;
+  } else {
+    unsigned beforewildcardlen = wildcard - p;
+    unsigned afterwildcardlen = strlen(wildcard + 1);
+    unsigned slen = strlen(s);
+    return (slen >= beforewildcardlen + afterwildcardlen) && 
+      (strncmp(s,p,beforewildcardlen) == 0) && strcmp(s + slen - afterwildcardlen,wildcard + 1) == 0;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Parse addresses from a string.  Returns true if at least one address found
+//
+////////////////////////////////////////////////////////////////////////////////
+bool ParseAddresses(string str,vector<string> &Addresses)
+{
+  char* s = (char* )str.c_str();
+
+  while (*s != '\0') {
+    char* start = s;
+    char* from = s;
+    char* to = s;
+    char* lt = NULL;  // pointer to less than character (<) which starts the address if found
+
+    while (*from != '\0') {
+      if (*from == '(') {
+        // skip over comment
+        from++;
+        for (int depth = 1; depth != 0; from++) {
+          if (*from == '\0')
+            break;
+          else if (*from == '(')
+            depth++;
+          else if (*from == ')')
+            depth--;
+          else if (*from == '\\' && from[1] != '\0')
+            from++;
+        }
+      }
+      else if (*from == ')') {
+        // ignore closing parenthesis outside of comment
+        from++;
+      } else if (*from == ',' || *from == ';') {
+        // comma/semicolon ends the address
+        from++;
+        break;
+      }
+      else if (*from == ' ' || *from == '\t' || *from == '\r' || *from == '\n') {
+        // ignore whitespace
+        from++;
+      } else if (*from == '"') {
+        // copy the contents of a quoted string
+        from++;
+        while (*from != '\0') {
+          if (*from == '"') {
+            from++;
+            break;
+          } else if (*from == '\\' && from[1] != '\0')
+            *to++ = *from++;
+          *to++ = *from++;
+        }
+      } else if (*from == '\\' && from[1] != '\0') {
+        // copy quoted-pair
+        *to++ = *from++;
+        *to++ = *from++;
+      } else {
+        // copy any other char
+        *to = *from++;
+        // save pointer to '<' for later...
+        if (*to == '<')
+          lt = to;
+        to++;
+      }
+    }
+
+    *to = '\0';
+
+    // if there's < > get what's inside
+    if (lt != NULL) {
+      start = lt+1;
+      char *gt = strchr(start, '>');
+      if (gt != NULL)
+        *gt = '\0';
+    } else {
+      // look for and strip group name
+      char *colon = strchr(start, ':');
+      if (colon != NULL) {
+        char *at = strchr(start, '@');
+        if (at == NULL || colon < at)
+          start = colon+1;
+      }
+    }
+
+    if (*start != '\0' && strchr(start, '@') != NULL) {
+      Addresses.push_back(start);   // save address
+    }
+
+    s = from;
+  }
+
+  return !Addresses.empty();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+CDKIMVerify::CDKIMVerify()
+{
+  m_pfnSelectorCallback = NULL;
+//  m_pfnPracticesCallback = NULL;
+  m_HonorBodyLengthTag = false;
+  m_CheckPractices = false;
+// Kai:
+//  m_SubjectIsRequired = true;
+  m_SubjectIsRequired = false;
+  m_SaveCanonicalizedData = false;
+  m_AllowUnsignedFromHeaders = false;
+}
+
+CDKIMVerify::~CDKIMVerify() {} // Destructor
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Init - save the options
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::Init(DKIMVerifyOptions* pOptions)
+{
+  int nRet = CDKIMBase::Init();
+
+  m_pfnSelectorCallback = pOptions->pfnSelectorCallback;
+  // m_pfnPracticesCallback = pOptions->pfnPracticesCallback;
+
+  m_HonorBodyLengthTag = pOptions->nHonorBodyLengthTag != 0;
+  m_CheckPractices = pOptions->nCheckPractices != 0;
+// Kai:
+//  m_SubjectIsRequired = pOptions->nSubjectRequired == 0;
+  m_SubjectIsRequired = pOptions->nSubjectRequired == 1;
+  m_SaveCanonicalizedData = pOptions->nSaveCanonicalizedData != 0;
+  m_AllowUnsignedFromHeaders = pOptions->nAllowUnsignedFromHeaders != 0;
+
+  return nRet;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// GetResults - return the pass/fail/neutral verification result
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::GetResults(void)
+{
+  // char mdi[128];
+  // char digi[128];
+   
+  ProcessFinal();
+
+  unsigned char *SignMsg;
+  unsigned SuccessCount = 0;
+  int TestingFailures = 0;
+  int RealFailures = 0;
+  int res = 0;
+
+  list<string> SuccessfulDomains;  // can contain duplicates
+
+  for (list<SignatureInfo>::iterator i = Signatures.begin(); i != Signatures.end(); ++i) {
+    if (i->Status == DKIM_SUCCESS) {
+      if (!i->BodyHashData.empty()) {  // FIRST: Get the body hash
+        unsigned char md[EVP_MAX_MD_SIZE];
+        unsigned len = 0;
+
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+        res = EVP_DigestFinal(&i->m_Bdy_ctx,md,&len);
+#else
+        res = EVP_DigestFinal_ex(i->m_Bdy_ctx,md,&len);  
+        EVP_MD_CTX_reset(i->m_Bdy_ctx);
+#endif
+        // dig_ascii(digi,md,32);
+        // dig_ascii(mdi,(unsigned const char *)i->BodyHashData.data(),32);
+
+        if (!res || len != i->BodyHashData.length() || memcmp(i->BodyHashData.data(),md,len) != 0) {
+          // body hash mismatch
+
+          // if the selector is in testing mode...
+          if (i->m_pSelector->Testing) {
+            i->Status = DKIM_SIGNATURE_BAD_BUT_TESTING;  // todo: make a new error code for this?
+            TestingFailures++;
+          } else {
+            i->Status = DKIM_BODY_HASH_MISMATCH;
+            RealFailures++;
+          }
+          continue;  // next signature
+        }
+      } else {
+        // hash CRLF separating the body from the signature
+        i->Hash("\r\n",2);
+      }
+
+      // SECOND: Fetch the signature
+
+      string sSignedSig = i->Header;
+      string sSigValue = sSignedSig.substr(sSignedSig.find(':') + 1);
+
+      static const char* tags[] = {"b",NULL};
+      char* values[sizeof(tags)/sizeof(tags[0])] = {NULL};
+
+      char* pSigValue = (char* ) sSigValue.c_str();   // our signature
+      if (ParseTagValueList(pSigValue,tags,values) && values[0] != NULL) {
+        sSignedSig.erase(15 + values[0] - pSigValue,strlen(values[0]));
+      }
+
+      if (i->HeaderCanonicalization == DKIM_CANON_RELAXED) {
+        sSignedSig = RelaxHeader(sSignedSig);
+      }
+      else if (i->HeaderCanonicalization == DKIM_CANON_NOWSP) {
+        RemoveSWSP(sSignedSig);
+        // convert "DKIM-Signature" to lower case
+        sSignedSig.replace(0,14,"dkim-signature",14);
+      }
+
+      i->Hash(sSignedSig.c_str(),sSignedSig.length());   // include generated DKIM signature header 
+      assert(i->m_pSelector != NULL);
+
+      if (EVP_PKEY_base_id(i->m_pSelector->PublicKey) != EVP_PKEY_ED25519) 
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+        res = EVP_VerifyFinal(&i->m_Hdr_ctx,(unsigned char *)i->SignatureData.data(),i->SignatureData.length(),i->m_pSelector->PublicKey);
+#else
+        res = EVP_VerifyFinal(i->m_Hdr_ctx,(unsigned char *)i->SignatureData.data(),i->SignatureData.length(),i->m_pSelector->PublicKey);
+#endif
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L)
+      else if (EVP_PKEY_base_id(i->m_pSelector->PublicKey) == EVP_PKEY_ED25519) {
+        EVP_DigestVerifyInit(i->m_Msg_ctx,NULL,NULL,NULL,i->m_pSelector->PublicKey);  // late initialization
+
+        SignMsg = (unsigned char *) SigHdr.data();
+        res = EVP_DigestVerify(i->m_Msg_ctx,(unsigned char *)i->SignatureData.data(),(size_t)i->SignatureData.length(),
+                               SignMsg,m_SigHdr);
+      }
+#endif
+
+      if (res == 1) {
+        if (i->UnverifiedBodyCount == 0)
+          i->Status = DKIM_SUCCESS;
+        else
+          i->Status = DKIM_SUCCESS_BUT_EXTRA;
+        SuccessCount++;
+        SuccessfulDomains.push_back(i->Domain);
+      } else {
+        // if the selector is in testing mode...
+        if (i->m_pSelector->Testing) {
+          i->Status = DKIM_SIGNATURE_BAD_BUT_TESTING;
+          TestingFailures++;
+        } else {
+          i->Status = DKIM_SIGNATURE_BAD;
+          RealFailures++;
+        }
+      }
+    } else if (i->Status == DKIM_SELECTOR_GRANULARITY_MISMATCH || 
+               i->Status == DKIM_SELECTOR_ALGORITHM_MISMATCH || 
+               i->Status == DKIM_SELECTOR_KEY_REVOKED) {
+      // treat these as failures
+      // todo: maybe see if the selector is in testing mode?
+      RealFailures++;
+    }
+  }    // loop over signature infos done
+
+
+  // get the From address's domain if we might need it
+  string sFromDomain;
+  if (SuccessCount > 0 || m_CheckPractices) {
+    for (list<string>::iterator i = HeaderList.begin(); i != HeaderList.end(); ++i) {
+      if (_strnicmp(i->c_str(),"From",4) == 0) {
+        // skip over whitespace between the header name and :
+        const char* s = i->c_str() + 4;
+        while (*s == ' ' || *s == '\t')
+          s++;
+        if (*s == ':') {
+          vector<string> Addresses;
+          if (ParseAddresses(s + 1, Addresses)) {
+            unsigned atpos = Addresses[0].find('@');
+            sFromDomain = Addresses[0].substr(atpos + 1);
+            break;
+          }
+        }
+      }
+    }
+  }
+ 
+  // if a signature from the From domain verified successfully, return success now
+  // without checking the author domain signing practices
+  if (SuccessCount > 0 && !sFromDomain.empty()) {
+    for (list<string>::iterator i = SuccessfulDomains.begin(); i != SuccessfulDomains.end(); ++i) {
+      // see if the successful domain is the same as or a parent of the From domain
+      if (i->length() > sFromDomain.length())
+        continue;
+      if (_stricmp(i->c_str(),sFromDomain.c_str() + sFromDomain.length() - i->length()) != 0)
+        continue;
+      if (i->length() == sFromDomain.length() || sFromDomain.c_str()[sFromDomain.length() - i->length() - 1] == '.') {
+        return SuccessCount == Signatures.size() ? DKIM_SUCCESS : DKIM_PARTIAL_SUCCESS;
+      }
+    }
+  }
+
+  /* Removed obsolete ADSP check */
+
+  // return neutral for everything else
+  return DKIM_NEUTRAL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Hash - update the hash or update the Ed25519 signature input
+//
+////////////////////////////////////////////////////////////////////////////////
+void SignatureInfo::Hash(const char* szBuffer,unsigned nBufLength,bool IsBody)
+{
+#if 0
+  /** START DEBUG CODE **/
+  if(nBufLength == 2 && szBuffer[0] == '\r' && szBuffer[1] == '\n')
+  {
+    printf("[CRLF]\n");
+  } else {
+    char* szDbg = new char[nBufLength+1];
+    strncpy(szDbg, szBuffer, nBufLength);
+    szDbg[nBufLength] = '\0';
+    printf("[%s]\n", szDbg);
+  }
+  /** END DEBUG CODE **/
+#endif
+
+  if (IsBody && BodyLength != (unsigned) -1) {    // trick: 2's complement 
+    VerifiedBodyCount += nBufLength;
+    if (VerifiedBodyCount > BodyLength) {
+      nBufLength = BodyLength - (VerifiedBodyCount - nBufLength);
+      UnverifiedBodyCount += VerifiedBodyCount - BodyLength;
+      VerifiedBodyCount = BodyLength;
+      if (nBufLength == 0) return;
+    }
+  }
+
+  if (IsBody && !BodyHashData.empty()) {
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+    EVP_DigestUpdate(&m_Bdy_ctx,szBuffer,nBufLength);
+  } else {
+    EVP_VerifyUpdate(&m_Hdr_ctx,szBuffer,nBufLength);
+#else
+    EVP_DigestUpdate(m_Bdy_ctx,szBuffer,nBufLength);
+  } else {
+    EVP_VerifyUpdate(m_Hdr_ctx,szBuffer,nBufLength);
+#endif
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L)
+    SigHdr.append(szBuffer,nBufLength);
+    m_SigHdr += nBufLength;
+#endif
+  }
+
+  if (m_SaveCanonicalizedData) {
+    CanonicalizedData.append(szBuffer,nBufLength);
+  }
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// ProcessHeaders - Look for DKIM-Signatures and start processing them
+//                  look for DKIM-Signature header(s)
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::ProcessHeaders(void)
+{
+  for (list<string>::iterator i = HeaderList.begin(); i != HeaderList.end(); ++i) {
+    if (strlen(i->c_str()) < 14) continue; // too short
+    if (_strnicmp(i->c_str(),"DKIM-Signature",14) == 0) {
+      // skip over whitespace between the header name and :
+      const char *s = i->c_str() + 14;
+      while (*s == ' ' || *s == '\t')
+        s++;
+      if (*s == ':') {
+        // found
+        SignatureInfo sig(m_SaveCanonicalizedData);
+        sig.Status = ParseDKIMSignature(*i,sig);
+        Signatures.push_back(sig);    // save signature
+
+        if (Signatures.size() >= MAX_SIGNATURES)
+          break;
+      }
+    }
+  }
+
+  if (Signatures.empty())
+    return DKIM_NO_SIGNATURES;
+
+  bool ValidSigFound = false;
+
+  for (list<SignatureInfo>::iterator s = Signatures.begin(); s != Signatures.end(); ++s) {
+    SignatureInfo &sig = *s;
+    if (sig.Status != DKIM_SUCCESS) continue;
+    SelectorInfo &sel = GetSelector(sig.Selector,sig.Domain);
+    sig.m_pSelector = &sel;
+
+    if (sel.Status != DKIM_SUCCESS) {
+      sig.Status = sel.Status;
+    } else {
+      // check the granularity
+      if (!WildcardMatch(sel.Granularity.c_str(),sig.IdentityLocalPart.c_str()))
+        sig.Status = DKIM_SELECTOR_GRANULARITY_MISMATCH;    // this error causes the signature to fail
+
+      // check the hash algorithm
+      if ((sig.m_nHash == DKIM_HASH_SHA1 && !sel.AllowSHA1) || 
+          (sig.m_nHash == DKIM_HASH_SHA256 && !sel.AllowSHA256))
+        sig.Status = DKIM_SELECTOR_ALGORITHM_MISMATCH;      // causes signature to fail
+
+      // check for same domain
+      if (sel.SameDomain && _stricmp(sig.Domain.c_str(),sig.IdentityDomain.c_str()) != 0)
+        sig.Status = DKIM_BAD_SYNTAX;
+    }
+
+    if (sig.Status != DKIM_SUCCESS) continue;
+
+    // initialize the hashes
+    if (sig.m_nHash == DKIM_HASH_SHA1) {
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+      EVP_VerifyInit(&sig.m_Hdr_ctx,EVP_sha1());
+      EVP_DigestInit(&sig.m_Bdy_ctx,EVP_sha1());
+#else
+      EVP_VerifyInit_ex(sig.m_Hdr_ctx,EVP_sha1(),NULL);
+      EVP_DigestInit_ex(sig.m_Bdy_ctx,EVP_sha1(),NULL);
+#endif
+    }
+    if (sig.m_nHash == DKIM_HASH_SHA256) {
+#if ((OPENSSL_VERSION_NUMBER < 0x10100000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+      EVP_VerifyInit(&sig.m_Hdr_ctx,EVP_sha256());
+      EVP_DigestInit(&sig.m_Bdy_ctx,EVP_sha256());
+#else
+      EVP_VerifyInit_ex(sig.m_Hdr_ctx,EVP_sha256(),NULL);
+      EVP_DigestInit_ex(sig.m_Bdy_ctx,EVP_sha256(),NULL);
+#endif
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L) 
+      SigHdr.assign("");
+      m_SigHdr = 0;
+    }
+#endif
+
+    // compute the hash of the header
+    vector<list<string>::reverse_iterator> used;
+
+    for (vector<string>::iterator x = sig.SignedHeaders.begin(); x != sig.SignedHeaders.end(); ++x) {
+      list<string>::reverse_iterator i;
+      for (i = HeaderList.rbegin(); i != HeaderList.rend(); ++i) {
+        if (_strnicmp(i->c_str(),x->c_str(),x->length()) == 0) {
+          // skip over whitespace between the header name and :
+          const char* s = i->c_str()+x->length();
+          while (*s == ' ' || *s == '\t')
+            s++;
+          if (*s == ':' && find(used.begin(),used.end(),i) == used.end())
+            break;
+        }
+      }
+
+      if (i != HeaderList.rend()) {
+        used.push_back(i);
+
+        // hash this header
+        if (sig.HeaderCanonicalization == DKIM_CANON_SIMPLE) {
+          sig.Hash(i->c_str(),i->length());
+        } else if (sig.HeaderCanonicalization == DKIM_CANON_RELAXED) {
+          string sTemp = RelaxHeader(*i);
+          sig.Hash(sTemp.c_str(),sTemp.length());
+        } else if (sig.HeaderCanonicalization == DKIM_CANON_NOWSP) {
+          string sTemp = *i;
+          RemoveSWSP(sTemp);
+
+          // convert characters before ':' to lower case
+          for (char* s = (char*)sTemp.c_str(); *s != '\0' && *s != ':'; s++) {
+            if (*s >= 'A' && *s <= 'Z')
+              *s += 'a' - 'A';
+          }
+          sig.Hash(sTemp.c_str(),sTemp.length());
+        }
+        sig.Hash("\r\n",2);
+      }
+    }
+
+    if (sig.BodyHashData.empty()) {
+      // hash CRLF separating headers from body
+      sig.Hash("\r\n",2);
+    }
+
+    if (!m_AllowUnsignedFromHeaders) {
+      // make sure the message has no unsigned From headers
+      list<string>::reverse_iterator i;
+      for (i = HeaderList.rbegin(); i != HeaderList.rend(); ++i) {
+        if (_strnicmp(i->c_str(),"From",4) == 0) {
+          // skip over whitespace between the header name and :
+          const char *s = i->c_str() + 4;
+          while (*s == ' ' || *s == '\t')
+            s++;
+          if (*s == ':') {
+            if (find(used.begin(),used.end(),i) == used.end()) {
+              // this From header was not signed
+              break;
+            }
+          }
+        }
+      }
+      if (i != HeaderList.rend()) {
+        // treat signature as invalid
+        sig.Status = DKIM_UNSIGNED_FROM;
+        continue;
+      }
+    }
+
+    ValidSigFound = true;
+  }
+
+  if (!ValidSigFound)
+    return DKIM_NO_VALID_SIGNATURES;
+
+  return DKIM_SUCCESS;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// Strictly parse an unsigned integer.  Don't allow spaces, negative sign,
+// 0x prefix, etc.  Values greater than 2^32-1 are capped at 2^32-1
+//
+////////////////////////////////////////////////////////////////////////////////
+bool ParseUnsigned(const char *s, unsigned *result)
+{
+  unsigned temp = 0, last = 0;
+  bool overflowed = false;
+
+  do {
+    if (*s < '0' || *s > '9')
+      return false;        // returns false for an initial '\0'
+
+    temp = temp * 10 + (*s - '0');
+    if (temp < last)
+      overflowed = true;
+    last = temp;
+
+    s++;
+  } while (*s != '\0');
+
+  *result = overflowed ? -1 : temp;
+  return true;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// ParseDKIMSignature - Parse a DKIM-Signature header field
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::ParseDKIMSignature(const string& sHeader,SignatureInfo &sig)
+{
+  // for strtok_r()
+  char *saveptr;
+
+  // save header for later
+  sig.Header = sHeader;
+
+  string sValue = sHeader.substr(sHeader.find(':') + 1);
+
+  static const char *tags[] = {"v","a","b","d","h","s","c","i","l","q","t","x","bh",NULL};
+  char *values[sizeof(tags)/sizeof(tags[0])] = {NULL};
+
+  if (!ParseTagValueList((char*) sValue.c_str(),tags,values))
+    return DKIM_BAD_SYNTAX;
+
+  // check signature version
+  if (values[0] == NULL) return DKIM_BAD_SYNTAX;
+  
+  // signature MUST have a=, b=, d=, h=, s=
+  if (values[1] == NULL || values[2] == NULL || values[3] == NULL || values[4] == NULL || values[5] == NULL)
+    return DKIM_BAD_SYNTAX;
+
+  // algorithm ('a=') can be "rsa-sha1" or "rsa-sha256" or "ed25519"
+  if (strcmp(values[1],"rsa-sha1") == 0) {
+    sig.m_nHash = DKIM_HASH_SHA1;
+  } else if (strcmp(values[1],"rsa-sha256") == 0) {
+    sig.m_nHash = DKIM_HASH_SHA256;
+#if (OPENSSL_VERSION_NUMBER > 0x10101000L) 
+  } else if (strcmp(values[1],"ed25519-sha256") == 0) {
+    sig.m_nHash = DKIM_HASH_SHA256;
+#endif
+  } else {
+    return DKIM_BAD_SYNTAX;    // todo: maybe create a new error code for unknown algorithm
+  }
+
+  // make sure the signature data is not empty: b=[...]
+  unsigned SigDataLen = DecodeBase64(values[2]);
+
+  if (SigDataLen == 0)
+    return DKIM_BAD_SYNTAX;
+
+  sig.SignatureData.assign(values[2],SigDataLen);
+
+  // check for body hash in DKIM header: bh=[...];
+  unsigned BodyHashLen = DecodeBase64(values[12]);
+  if (BodyHashLen == 0) return DKIM_BAD_SYNTAX;
+  sig.BodyHashData.assign(values[12],BodyHashLen);
+
+  // domain must not be empty
+  if (*values[3] == '\0')
+    return DKIM_BAD_SYNTAX;
+  sig.Domain = values[3];
+
+  // signed headers must not be empty (more verification is done later)
+  if (*values[4] == '\0')
+    return DKIM_BAD_SYNTAX;
+
+  // selector must not be empty
+  if (*values[5] == '\0')
+    return DKIM_BAD_SYNTAX;
+  sig.Selector = values[5];
+
+  // canonicalization
+  if (values[6] == NULL) {
+    sig.HeaderCanonicalization = sig.BodyCanonicalization = DKIM_CANON_SIMPLE;
+  } else {
+    char* slash = strchr(values[6],'/');
+    if (slash != NULL)
+      *slash = '\0';
+
+    if (strcmp(values[6],"simple") == 0)
+      sig.HeaderCanonicalization = DKIM_CANON_SIMPLE;
+    else if (strcmp(values[6],"relaxed") == 0)
+      sig.HeaderCanonicalization = DKIM_CANON_RELAXED;
+    else
+      return DKIM_BAD_SYNTAX;
+
+    if (slash == NULL || strcmp(slash + 1,"simple") == 0)
+      sig.BodyCanonicalization = DKIM_CANON_SIMPLE;
+    else if (strcmp(slash + 1,"relaxed") == 0)
+      sig.BodyCanonicalization = DKIM_CANON_RELAXED;
+    else
+      return DKIM_BAD_SYNTAX;
+  }
+
+  // identity
+  if (values[7] == NULL) {
+    sig.IdentityLocalPart.erase();
+    sig.IdentityDomain = sig.Domain;
+  } else {
+    // quoted-printable decode the value
+    DecodeQuotedPrintable(values[7]);
+
+    // must have a '@' separating the local part from the domain
+    char* at = strchr(values[7],'@');
+    if (at == NULL)
+      return DKIM_BAD_SYNTAX;
+    *at = '\0';
+
+    char* ilocalpart = values[7];
+    char* idomain = at + 1;
+
+    // i= domain must be the same as or a subdomain of the d= domain
+    int idomainlen = strlen(idomain);
+    int ddomainlen = strlen(values[3]);
+
+    // todo: maybe create a new error code for invalid identity domain
+    if (idomainlen < ddomainlen)
+      return DKIM_BAD_SYNTAX;
+    if (_stricmp(idomain + idomainlen - ddomainlen,values[3]) != 0)
+      return DKIM_BAD_SYNTAX;
+    if (idomainlen > ddomainlen && idomain[idomainlen - ddomainlen - 1] != '.')
+      return DKIM_BAD_SYNTAX;
+
+    sig.IdentityLocalPart = ilocalpart;
+    sig.IdentityDomain = idomain;
+  }
+
+  // body count
+  if (values[8] == NULL || !m_HonorBodyLengthTag) {
+    sig.BodyLength = (unsigned) -1;
+  } else {
+    if (!ParseUnsigned(values[8],&sig.BodyLength))
+      return DKIM_BAD_SYNTAX;
+  }
+
+  // query methods
+  if (values[9] != NULL) {
+    // make sure "dns" is in the list
+    bool HasDNS = false;
+    char* s = strtok_r(values[9],":",&saveptr);
+    while (s != NULL) {
+      if (strncmp(s,"dns",3) == 0 && (s[3] == '\0' || s[3] == '/')) {
+        HasDNS = true;
+        break;
+      }
+      s = strtok_r(NULL,": \t",&saveptr);  /* FIXME */
+//      s = strtok_r(NULL,":   ",&saveptr);  /* FIXME */
+    }
+    if (!HasDNS)
+      return DKIM_BAD_SYNTAX;    // todo: maybe create a new error code for unknown query method
+  }
+
+  // signature time
+  unsigned SignedTime = -1;
+  if (values[10] != NULL) {
+    if (!ParseUnsigned(values[10],&SignedTime))
+      return DKIM_BAD_SYNTAX;
+  }
+
+  // expiration time
+  if (values[11] == NULL) {
+    sig.ExpireTime = (unsigned) -1;   // common trick; feh
+  } else {
+    if (!ParseUnsigned(values[11],&sig.ExpireTime))
+      return DKIM_BAD_SYNTAX;
+
+    if (sig.ExpireTime != (unsigned) -1) {
+      // the value of x= MUST be greater than the value of t= if both are present
+      if (SignedTime != (unsigned) -1 && sig.ExpireTime <= SignedTime)
+        return DKIM_BAD_SYNTAX;
+
+      // todo: if possible, use the received date/time instead of the current time
+      unsigned curtime = time(NULL);
+      if (curtime > sig.ExpireTime)
+        return DKIM_SIGNATURE_EXPIRED;
+    }
+  }
+
+  // parse the signed headers list
+  bool HasFrom = false, HasSubject = false;
+  RemoveSWSP(values[4]);      // header names shouldn't have spaces in them so this should be ok...
+  char* s = strtok_r(values[4],":",&saveptr);
+  while (s != NULL) {
+    if (_stricmp(s,"From") == 0)
+      HasFrom = true;
+    else if (_stricmp(s,"Subject") == 0)
+      HasSubject = true;
+
+    sig.SignedHeaders.push_back(s);
+    s = strtok_r(NULL,":",&saveptr);
+  }
+
+  if (!HasFrom)
+    return DKIM_BAD_SYNTAX;    // todo: maybe create a new error code for h= missing From
+  if (m_SubjectIsRequired && !HasSubject)
+    return DKIM_BAD_SYNTAX;    // todo: maybe create a new error code for h= missing Subject
+
+  return DKIM_SUCCESS;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// ProcessBody - Process message body data
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::ProcessBody(char* szBuffer,int nBufLength,bool bEOF)
+{
+  bool MoreBodyNeeded = false;
+
+  for (list<SignatureInfo>::iterator i = Signatures.begin(); i != Signatures.end(); ++i) {
+    if (i->Status == DKIM_SUCCESS) {
+      if (i->BodyCanonicalization == DKIM_CANON_SIMPLE) {
+        if (nBufLength > 0) {
+          while (i->EmptyLineCount > 0) {
+            i->Hash("\r\n",2,true);
+            i->EmptyLineCount--;
+          }
+          i->Hash(szBuffer,nBufLength,true);
+          i->Hash("\r\n",2,true);
+        } else {
+          i->EmptyLineCount++;
+          if (bEOF)
+            i->Hash("\r\n",2,true);
+        }
+      } else if (i->BodyCanonicalization == DKIM_CANON_RELAXED) {
+        CompressSWSP(szBuffer, nBufLength);
+        if (nBufLength > 0) {
+          while (i->EmptyLineCount > 0) {
+            i->Hash("\r\n",2,true);
+            i->EmptyLineCount--;
+          }
+          i->Hash(szBuffer,nBufLength,true);
+          if (!bEOF)
+            i->Hash("\r\n",2,true);
+        } else i->EmptyLineCount++;
+      } else if (i->BodyCanonicalization == DKIM_CANON_NOWSP) {
+        RemoveSWSP(szBuffer,nBufLength);
+        i->Hash(szBuffer,nBufLength,true);
+      }
+
+      if (i->UnverifiedBodyCount == 0)
+        MoreBodyNeeded = true;
+    }
+  }
+
+  if (!MoreBodyNeeded)
+    return DKIM_FINISHED_BODY;
+
+  return DKIM_SUCCESS;
+}
+
+SelectorInfo::SelectorInfo(const string &sSelector,const string &sDomain) : Domain(sDomain),Selector(sSelector)
+{
+  AllowSHA1 = true;
+  AllowSHA256 = true;
+  PublicKey = NULL;
+  Testing = false;
+  SameDomain = false;
+  Status = DKIM_SUCCESS;
+}
+
+SelectorInfo::~SelectorInfo()
+{
+  if (PublicKey != NULL) {
+    EVP_PKEY_free(PublicKey);
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// Parse - Parse a DKIM selector from DNS data
+//
+////////////////////////////////////////////////////////////////////////////////
+int SelectorInfo::Parse(char* Buffer)
+{
+  // for strtok_r()
+  char *saveptr;
+  char *PubKeyBase64; /*- public key Base64 encoded */
+  char ed25519PubKey[61];
+
+  static const char *tags[] = {"v","g","h","k","p","s","t","n",NULL}; // 0, 1, 2, 3, 4
+  char *values[sizeof(tags)/sizeof(tags[0])] = {NULL};
+
+  ParseTagValueList(Buffer,tags,values);
+   
+  //  return DKIM_SELECTOR_INVALID;
+  if (values[0] != NULL) {
+    // make sure the version is "DKIM1"
+    if (strcmp(values[0],"DKIM1") != 0)
+      return DKIM_SELECTOR_INVALID;    // todo: maybe create a new error code for unsupported selector version
+
+    // make sure v= is the first tag in the response  // todo: maybe don't enforce this, it seems unnecessary
+    for (unsigned j = 1; j < sizeof(values)/sizeof(values[0]); j++) {
+      if (values[j] != NULL && values[j] < values[0]) {
+        return DKIM_SELECTOR_INVALID;
+      }
+    }
+  }
+
+  // selector MUST have p= tag
+  if (values[4] == NULL)
+    return DKIM_SELECTOR_INVALID;
+
+  PubKeyBase64 = values[4]; // gotcha
+
+  // granularity -- [g= ... ]
+  if (values[1] == NULL)
+    Granularity = "*";
+  else
+    Granularity = values[1];
+
+  // hash algorithm -- [h=sha1|sha256] (not required)
+  if (values[2] == NULL) {
+    AllowSHA1 = true;
+    AllowSHA256 = true;
+  } else {
+    // MUST include "sha1" or "sha256"
+    char* s = strtok_r(values[2],":",&saveptr);
+    while (s != NULL) {
+      if (strcmp(s,"sha1") == 0)
+        { AllowSHA1 = true; AllowSHA256 = false; }
+      else if (strcmp(s,"sha256") == 0)
+        { AllowSHA256 = true; AllowSHA1 = false; }
+      s = strtok_r(NULL,":",&saveptr);
+    }
+    if (!(AllowSHA1 || AllowSHA256))
+      return DKIM_SELECTOR_INVALID;  // todo: maybe create a new error code for unsupported hash algorithm
+  }
+
+  // key type -- [k=rsa|ed25519]  (not required)
+  if (values[3] != NULL) {
+    // key type MUST be "rsa" or "ed25519"
+    if (strcmp(values[3],"rsa") != 0 && strcmp(values[3],"ed25519") != 0) // none of either
+      return DKIM_SELECTOR_INVALID;
+    if (strcmp(values[3],"ed25519") == 0) { 
+      AllowSHA1 = false; 
+      AllowSHA256 = true; 
+      strcpy(ed25519PubKey,"MCowBQYDK2VwAyEA");
+      /*
+       * rfc8463
+       * since Ed25519 public keys are 256 bits long,
+       * the base64-encoded key is only 44 octets
+       */
+      if (strlen(values[4]) > 44)
+        return DKIM_SELECTOR_PUBLIC_KEY_INVALID;
+      strcat(ed25519PubKey,values[4]);
+      PubKeyBase64 = ed25519PubKey;
+    } 
+  }
+
+  // service type -- [s= ...] (not required)
+  if (values[5] != NULL) {
+    // make sure "*" or "email" is in the list
+    bool ServiceTypeMatch = false;
+    char* s = strtok_r(values[5],":",&saveptr);
+    while (s != NULL) {
+      if (strcmp(s, "*") == 0 || strcmp(s,"email") == 0) {
+        ServiceTypeMatch = true;
+        break;
+      }
+      s = strtok_r(NULL,":",&saveptr);
+    }
+    if (!ServiceTypeMatch)
+      return DKIM_SELECTOR_INVALID;
+  }
+
+  // flags -- [t= ...] (not required)
+  if (values[6] != NULL) {
+    char *s = strtok_r(values[6],":",&saveptr);
+    while (s != NULL) {
+      if (strcmp(s,"y") == 0) {
+        Testing = true;
+      } else if (strcmp(s,"s") == 0) {
+        SameDomain = true;
+      }
+      s = strtok_r(NULL,":",&saveptr);
+    }
+  }
+
+  // public key data
+  unsigned PublicKeyLen = DecodeBase64(PubKeyBase64);
+
+  if (PublicKeyLen == 0) {
+    return DKIM_SELECTOR_KEY_REVOKED;  // this error causes the signature to fail
+  } else {
+    const unsigned char *PublicKeyData = (unsigned char* )PubKeyBase64;  // 0-terminated
+
+    EVP_PKEY *pkey = d2i_PUBKEY(NULL,&PublicKeyData,PublicKeyLen);  /* retrieve and return PubKey from data */
+
+    if (pkey == NULL)
+      return DKIM_SELECTOR_PUBLIC_KEY_INVALID;
+
+    // make sure public key is the correct type (we only support rsa & ed25519)
+#if ((OPENSSL_VERSION_NUMBER < 0x10101000L) || (LIBRESSL_VERSION_NUMBER > 0 && LIBRESSL_VERSION_NUMBER < 0x20700000L))
+    if (pkey->type == EVP_PKEY_RSA || pkey->type == EVP_PKEY_RSA2) {
+#else
+    if ((EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA)  || 
+        (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA2) ||
+        (EVP_PKEY_base_id(pkey) == EVP_PKEY_ED25519)) {
+#endif
+      PublicKey = pkey;
+    } else {
+      EVP_PKEY_free(pkey);
+      return DKIM_SELECTOR_PUBLIC_KEY_INVALID;
+    }
+  }
+
+  return DKIM_SUCCESS;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// GetSelector - Get a DKIM selector for a domain
+//
+////////////////////////////////////////////////////////////////////////////////
+SelectorInfo& CDKIMVerify::GetSelector(const string &sSelector,const string &sDomain)
+{
+  // see if we already have this selector
+  for (list<SelectorInfo>::iterator i = Selectors.begin(); i != Selectors.end(); ++i) {
+    if (_stricmp(i->Selector.c_str(),sSelector.c_str()) == 0 && _stricmp(i->Domain.c_str(),sDomain.c_str()) == 0) {
+      return *i;
+    }
+  }
+
+  Selectors.push_back(SelectorInfo(sSelector,sDomain));
+  SelectorInfo& sel = Selectors.back();
+
+  string sFQDN = sSelector;
+  sFQDN += "._domainkey.";
+  sFQDN += sDomain;
+
+  int BufLen = 1024; 
+  char Buffer[BufLen];
+
+  int DNSResult;
+
+  if (m_pfnSelectorCallback) { 
+    DNSResult = m_pfnSelectorCallback(sFQDN.c_str(),Buffer,BufLen);
+  } else
+    DNSResult = _DNSGetTXT(sFQDN.c_str(),Buffer,BufLen);
+
+//  Buffer++; BufLen--;
+
+  switch (DNSResult) {
+    case -1: case -2: case -3: case -5: sel.Status = DKIM_SELECTOR_DNS_TEMP_FAILURE; break;
+    case 0: case -6: sel.Status = DKIM_SELECTOR_DNS_PERM_FAILURE; break;
+    default: sel.Status = sel.Parse(Buffer);
+ }
+
+  return sel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 
+// GetDetails - Get DKIM verification details (per signature)
+//
+////////////////////////////////////////////////////////////////////////////////
+int CDKIMVerify::GetDetails(int* nSigCount,DKIMVerifyDetails** pDetails)
+{
+  Details.clear();
+
+  for (list < SignatureInfo>::iterator i = Signatures.begin(); i != Signatures.end(); ++i) {
+    DKIMVerifyDetails d;
+    d.szSignature = (char* )i->Header.c_str();
+    d.szSignatureDomain = (char* )i->Domain.c_str();
+    d.szIdentityDomain = (char* )i->IdentityDomain.c_str();
+    d.szCanonicalizedData = (char* )i->CanonicalizedData.c_str();
+    d.nResult = i->Status;
+    Details.push_back(d);
+  }
+
+  *nSigCount = Details.size();
+  *pDetails = (*nSigCount != 0) ? &Details[0] : NULL;
+
+  return DKIM_SUCCESS;
+}
-- 
cgit v1.2.3