summaryrefslogtreecommitdiff
path: root/src/dkimverify.cpp
diff options
context:
space:
mode:
authorJannis Hoffmann <jannis@fehcom.de>2024-07-09 11:44:11 +0200
committerJannis Hoffmann <jannis@fehcom.de>2024-07-09 11:44:11 +0200
commitf1b71c9fe7dbb4886588a036399cf5ebe16b7c47 (patch)
treee07786aa479c9fb6ee3e537078470aaab5454f80 /src/dkimverify.cpp
parenta293489ee83c8b05d845a162dc2a4de026f3775d (diff)
removed top level directory
Diffstat (limited to 'src/dkimverify.cpp')
-rw-r--r--src/dkimverify.cpp1443
1 files changed, 1443 insertions, 0 deletions
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;
+}