/***************************************************************************** * * 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 #include #include #include #include #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 &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 SuccessfulDomains; // can contain duplicates for (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 (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 == ':') { 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 (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 (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 (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 vector::reverse_iterator> used; for (vector::iterator x = sig.SignedHeaders.begin(); x != sig.SignedHeaders.end(); ++x) { 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 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 (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 (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 (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; }