/***************************************************************************** * * 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 "dkimverify.h" #include #include #include #include #include #include "dkim.h" #include "dnsgettxt.h" extern "C" { #include "stralloc.h" #include "dns.h" } using std::string; /***************************************************************************** * * 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 *, const char *); int dig_ascii(char *digascii, const unsigned 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) { // clang-format off 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, }; // clang-format on 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, std::vector& 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; std::list SuccessfulDomains; // can contain duplicates for (std::list::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 (std::list::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 == ':') { std::vector 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 (std::list::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 (std::list::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 (std::list::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 std::vector::reverse_iterator> used; for (std::vector::iterator x = sig.SignedHeaders.begin(); x != sig.SignedHeaders.end(); ++x) { std::list::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 std::list::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 (std::list::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 (std::list::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 (std::list::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; }