/* * GetPublicKey() * * Get the publickey from supported sources (ie. dns/txt) */ void Validatory::GetPublicKey(const DKIM::Signature& sig, DKIM::PublicKey& pubkey) throw (DKIM::PermanentError, DKIM::TemporaryError) { if (sig.GetQueryType() == DKIM::Signature::DKIM_Q_DNSTXT) { std::string query = sig.GetSelector() + "._domainkey." + sig.GetDomain(); std::string publicKey; if ((CustomDNSResolver? CustomDNSResolver(query, publicKey, CustomDNSData): DKIM::Util::Resolver().GetTXT(query, publicKey) )) { if (publicKey.empty()) throw DKIM::PermanentError(StringFormat("No key for signature %s._domainkey.%s", sig.GetSelector().c_str(), sig.GetDomain().c_str() ) ); pubkey.Parse(publicKey); return; } throw DKIM::TemporaryError(StringFormat("DNS query failed for %s._domainkey.%s", sig.GetSelector().c_str(), sig.GetDomain().c_str() ) ); } throw DKIM::PermanentError(StringFormat("Unsupported query type %d", (int)sig.GetQueryType() ) ); }
/* * GetSignature() * * Get the signature from supported header iterator */ void Validatory::GetSignature(const Message::HeaderList::const_iterator& headerIter, DKIM::Signature& sig) throw (DKIM::PermanentError) { sig.Parse(*headerIter); CheckBodyHash(sig); }
/* * CheckBodyHash() * * Validate the message according to rfc6376 */ void Validatory::CheckBodyHash(const DKIM::Signature& sig) throw (DKIM::PermanentError) { std::unique_ptr<EVP_MD_CTX, std::function<void(EVP_MD_CTX*)>> evpmdbody(EVP_MD_CTX_create(), [] (EVP_MD_CTX* p) { EVP_MD_CTX_destroy(p); }); // create signature for our body (message data) switch (sig.GetDigestAlgorithm()) { case DKIM::DKIM_A_SHA1: EVP_DigestInit_ex(evpmdbody.get(), EVP_sha1(), nullptr); break; case DKIM::DKIM_A_SHA256: EVP_DigestInit_ex(evpmdbody.get(), EVP_sha256(), nullptr); break; } DKIM::Conversion::EVPDigest evpupd; evpupd.ctx = evpmdbody.get(); CanonicalizationBody(m_file, sig.GetCanonModeBody(), m_msg.GetBodyOffset(), sig.GetBodySizeLimit(), sig.GetBodySize(), std::bind(&DKIM::Conversion::EVPDigest::update, &evpupd, std::placeholders::_1, std::placeholders::_2)); unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestFinal_ex(evpmdbody.get(), md_value, &md_len); if (sig.GetBodyHash().size() != md_len || memcmp(sig.GetBodyHash().c_str(), md_value, md_len) != 0) { throw DKIM::PermanentError("Body hash did not verify"); } }
/* * CheckSignature() * * Validate the message according to rfc6376 */ void Validatory::CheckSignature(const std::shared_ptr<DKIM::Header> header, const DKIM::Signature& sig, const DKIM::PublicKey& pub) throw (DKIM::PermanentError) { // sanity checking (between sig and pub) if (pub.GetDigestAlgorithms().size() > 0) if (find(pub.GetDigestAlgorithms().begin(), pub.GetDigestAlgorithms().end(), sig.GetDigestAlgorithm()) == pub.GetDigestAlgorithms().end()) throw DKIM::PermanentError("Algorithm is not allowed"); if (sig.GetSignatureAlgorithm() != pub.GetSignatureAlgorithm()) throw DKIM::PermanentError("Signature algorithm type mismatch"); if (!sig.IsARC()) { if (find(pub.GetFlags().begin(), pub.GetFlags().end(), "s") != pub.GetFlags().end()) if (sig.GetDomain() != sig.GetMailDomain()) throw DKIM::PermanentError("Domain must match sub-domain (flag s)"); } // create signature for our header std::unique_ptr<EVP_MD_CTX, std::function<void(EVP_MD_CTX*)>> evpmdhead(EVP_MD_CTX_create(), [] (EVP_MD_CTX* p) { EVP_MD_CTX_destroy(p); }); int md_nid; switch (sig.GetDigestAlgorithm()) { case DKIM::DKIM_A_SHA1: EVP_DigestInit_ex(evpmdhead.get(), EVP_sha1(), nullptr); md_nid = NID_sha1; break; case DKIM::DKIM_A_SHA256: EVP_DigestInit_ex(evpmdhead.get(), EVP_sha256(), nullptr); md_nid = NID_sha256; break; } CanonicalizationHeader canonicalhead(sig.GetCanonModeHeader()); // add all headers to our cache (they will be pop of the end) std::map<std::string, Message::HeaderList> headerCache; for (const auto & hIter : m_msg.GetHeaders()) { std::string headerName = hIter->GetName(); transform(headerName.begin(), headerName.end(), headerName.begin(), tolower); headerCache[headerName].push_back(hIter); } // add all signed headers to our hash for (auto name : sig.GetSignedHeaders()) { std::string tmp; transform(name.begin(), name.end(), name.begin(), tolower); std::map<std::string, Message::HeaderList>::iterator head = headerCache.find(name); // if this occurred // 1. we do not have a header of that name at all // 2. all headers with that name has been included... if (head == headerCache.end() || head->second.size() == 0) continue; #ifdef DEBUG printf("[%s]\n", canonicalhead.FilterHeader(head->second.back()->GetHeader()).c_str()); printf("[CRLF]\n"); #endif tmp = canonicalhead.FilterHeader(head->second.back()->GetHeader()) + "\r\n"; head->second.pop_back(); EVP_DigestUpdate(evpmdhead.get(), tmp.c_str(), tmp.size()); } // add our dkim-signature to the calculation (remove the "b"-tag) std::string h = header->GetHeader().substr(0, header->GetValueOffset()); std::string v = header->GetHeader().substr(header->GetValueOffset()); DKIM::TagListEntry bTag; sig.GetTag("b", bTag); v.erase((int)bTag.GetValueOffset(), bTag.GetValue().size()); std::string tmp = canonicalhead.FilterHeader(h + v); #ifdef DEBUG printf("[%s]\n", tmp.c_str()); #endif EVP_DigestUpdate(evpmdhead.get(), tmp.c_str(), tmp.size()); unsigned char md[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestFinal_ex(evpmdhead.get(), md, &md_len); // verify the header signature switch (sig.GetSignatureAlgorithm()) { case DKIM::DKIM_SA_RSA: { int r = RSA_verify(md_nid, md, md_len, (const unsigned char *)sig.GetSignatureData().c_str(), (unsigned int)sig.GetSignatureData().size(), pub.GetRSAPublicKey()); if (r != 1) throw DKIM::PermanentError("Signature did not verify"); } break; case DKIM::DKIM_SA_ED25519: if (crypto_sign_verify_detached((const unsigned char*)sig.GetSignatureData().c_str(), md, md_len, (const unsigned char *)pub.GetED25519PublicKey().c_str()) != 0) throw DKIM::PermanentError("Signature did not verify"); break; } // success! }
/* * GetSignature() * * Get the signature from supported header iterator */ void Validatory::GetSignature(const Message::HeaderList::const_iterator& headerIter, DKIM::Signature& sig) throw (DKIM::PermanentError) { sig.Parse((*headerIter)->GetHeader().substr((*headerIter)->GetValueOffset())); // create signature for our body (message data) switch (sig.GetAlgorithm()) { case DKIM::DKIM_A_SHA1: EVP_DigestInit(&m_ctx_body, EVP_sha1()); break; case DKIM::DKIM_A_SHA256: EVP_DigestInit(&m_ctx_body, EVP_sha256()); break; } CanonicalizationBody canonicalbody(sig.GetCanonModeBody()); // if we should limit the size of the body we hash bool limitBody = sig.GetBodySizeLimit(); size_t bodySize = sig.GetBodySize(); // if we have a message: seek to GetBodyOffset() if (m_msg.GetBodyOffset() != -1) { m_file.clear(); m_file.seekg(m_msg.GetBodyOffset(), std::istream::beg); std::string s; while (std::getline(m_file, s) || m_file.peek() != EOF) { // double dots (postfix file may have .., instead of .) if (m_doubleDots && s.substr(0, 2) == "..") { s.erase(0, 1); } // remove possible \r (if not removed by getline *probably not*) if (s.size() > 0 && s[s.size()-1] == '\r') s.erase(s.size()-1); // canonical body std::vector<std::string> output; if (canonicalbody.FilterLine(s, output)) { for (std::vector<std::string>::const_iterator i = output.begin(); i != output.end(); ++i) { if (limitBody && bodySize == 0) break; #ifdef DEBUG if (*i == "\r\n") printf("[CRLF]\n"); else printf("[%s]\n", i->c_str()); #endif EVP_DigestUpdate(&m_ctx_body, i->c_str(), limitBody?std::min(i->size(), bodySize):i->size()); bodySize -= std::min(i->size(), bodySize); } } } } // else call (Done) -- which may insert a last CRLF if the body was empty if (m_msg.GetBodyOffset() != -1 || m_file.peek() == EOF) { std::vector<std::string> output; if (canonicalbody.Done(output)) { for (std::vector<std::string>::const_iterator i = output.begin(); i != output.end(); ++i) { if (limitBody && bodySize == 0) break; #ifdef DEBUG if (*i == "\r\n") printf("[CRLF]\n"); else printf("[%s]\n", i->c_str()); #endif EVP_DigestUpdate(&m_ctx_body, i->c_str(), limitBody?std::min(i->size(), bodySize):i->size()); bodySize -= std::min(i->size(), bodySize); } } } unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestFinal_ex(&m_ctx_body, md_value, &md_len); EVP_MD_CTX_cleanup(&m_ctx_body); if (sig.GetBodyHash().size() != md_len || memcmp(sig.GetBodyHash().c_str(), md_value, md_len) != 0) { throw DKIM::PermanentError("Body hash did not verify"); } }
/* * GetADSP() * * Get ADSP status for message rfc5617 */ void Validatory::GetADSP(std::list<DKIM::ADSP>& adsp) throw (DKIM::PermanentError, DKIM::TemporaryError) { /** * Start by collecting all From: <> addresses (this runtime-time free...) */ std::list<std::string> senders; for (DKIM::Message::HeaderList::const_iterator i = m_msg.GetHeaders().begin(); i != m_msg.GetHeaders().end(); ++i) { std::string headerName = (*i)->GetName(); transform(headerName.begin(), headerName.end(), headerName.begin(), tolower); // find from: <...> header(s)... if (headerName == "from") { std::string header = (*i)->GetHeader().substr((*i)->GetValueOffset()); header = DKIM::Conversion::EncodedWord::Decode(header); std::list<std::string> addrlist = DKIM::Tokenizer::ParseAddressList(header); for (std::list<std::string>::const_iterator aIter = addrlist.begin(); aIter != addrlist.end(); ++aIter) { // if no address is claimed to follow a ADSP, try the next one if (aIter->empty()) continue; size_t atSign = aIter->rfind("@"); if (atSign == std::string::npos) throw DKIM::PermanentError("Found invalid sender address: " + *aIter); std::string host = aIter->substr(atSign + 1); transform(host.begin(), host.end(), host.begin(), tolower); senders.push_back(host); } } } std::map<std::string, std::pair<int, std::string> > dkimResult; for (Validatory::SignatureList::const_iterator i = GetSignatures().begin(); i != GetSignatures().end(); ++i) { DKIM::PublicKey pub; DKIM::Signature sig; try { GetSignature(i, sig); GetPublicKey(sig, pub); CheckSignature(i, sig, pub); dkimResult[sig.GetDomain()] = std::make_pair(1, "pass"); } catch (DKIM::TemporaryError& e) { dkimResult[sig.GetDomain()] = std::make_pair(-1, e.what()); } catch (DKIM::PermanentError& e) { if (pub.SoftFail()) dkimResult[sig.GetDomain()] = std::make_pair(1, e.what()); else dkimResult[sig.GetDomain()] = std::make_pair(0, e.what()); } } for (std::list<std::string>::const_iterator i = senders.begin(); i != senders.end(); ++i) { std::string query = "_adsp._domainkey." + *i; std::string adspRecord; ADSP tmp; tmp.SetDomain(*i); std::map<std::string, std::pair<int, std::string> >::const_iterator dIter = dkimResult.find(*i); if (dIter != dkimResult.end() && dIter->second.first == -1) { tmp.SetResult(ADSP::DKIM_ADSP_TEMPERROR, dIter->second.second); } else if (dIter != dkimResult.end() && dIter->second.first == 1) { tmp.SetResult(ADSP::DKIM_ADSP_PASS, dIter->second.second); } else { std::string error; if (dIter != dkimResult.end()) error = dIter->second.second; else error = "no dkim signature found for d=" + *i; if ((CustomDNSResolver? CustomDNSResolver(query, adspRecord, CustomDNSData): DKIM::Util::Resolver().GetTXT(query, adspRecord) )) { // only bad queries goes here.. if (!adspRecord.empty()) { try { TagList tagList; tagList.Parse(adspRecord); TagListEntry v; if (tagList.GetTag("dkim", v)) { if (v.GetValue() == "all") { tmp.SetResult(ADSP::DKIM_ADSP_FAIL, error); } else if (v.GetValue() == "discardable") { tmp.SetResult(ADSP::DKIM_ADSP_DISCARD, error); } else { tmp.SetResult(ADSP::DKIM_ADSP_UNKNOWN, error); } } else { tmp.SetResult(ADSP::DKIM_ADSP_UNKNOWN, error); } } catch (DKIM::TemporaryError& e) { tmp.SetResult(ADSP::DKIM_ADSP_TEMPERROR, e.what()); } catch (DKIM::PermanentError& e) { tmp.SetResult(ADSP::DKIM_ADSP_PERMERROR, e.what()); } } else { tmp.SetResult(ADSP::DKIM_ADSP_NONE, error); } } else { tmp.SetResult(ADSP::DKIM_ADSP_TEMPERROR, DKIM::Util::StringFormat("dns query failed for %s", query.c_str())); } } adsp.push_back(tmp); } }
/* * CheckSignature() * * Validate the message according to rfc4871 */ void Validatory::CheckSignature(const Message::HeaderList::const_iterator& headerIter, const DKIM::Signature& sig, const DKIM::PublicKey& pub) throw (DKIM::PermanentError) { // multiple signatures (must cleanup) EVP_MD_CTX_cleanup(&m_ctx_body); EVP_MD_CTX_cleanup(&m_ctx_head); // sanity checking (between sig and pub) if (pub.GetAlgorithms().size() > 0) if (find(pub.GetAlgorithms().begin(), pub.GetAlgorithms().end(), sig.GetAlgorithm()) == pub.GetAlgorithms().end()) throw DKIM::PermanentError("Algorithm is not allowed"); if (find(pub.GetFlags().begin(), pub.GetFlags().end(), "s") != pub.GetFlags().end()) if (sig.GetDomain() != sig.GetMailDomain()) throw DKIM::PermanentError("Domain must match sub-domain (flag s)"); /* If a DKIM verifier finds a selector record that has an empty "g" field ("g=;") and it does not have a "v" field ("v=DKIM1;") at its beginning, it is faced with deciding if this record was 1. from a DK signer that transitioned to supporting DKIM but forgot to remove the "g" field (so that it could be used by both DK and DKIM verifiers), or 2. from a DKIM signer that truly meant to use the empty "g" field but forgot to put in the "v" field. It is RECOMMENDED that you treat such records using the first interpretation, and treat such records as if the signer did not have a "g" field in the record. */ // if we have a "g"-tag if (find(pub.GetFlags().begin(), pub.GetFlags().end(), "g") != pub.GetFlags().end()) { // if it's empty... and we don't have a version... if (pub.GetMailLocalPart().empty() && find(pub.GetFlags().begin(), pub.GetFlags().end(), "v") == pub.GetFlags().end()) { // do the RECOMMENDED interpretation and treat such records as if the signer did not have a "g" field in the record. } else { if (DKIM::Util::MatchWithWildCard(pub.GetMailLocalPart(), sig.GetMailLocalPart()) != true) throw DKIM::PermanentError("Unmatched local-part"); } } // create signature for our header switch (sig.GetAlgorithm()) { case DKIM::DKIM_A_SHA1: EVP_VerifyInit(&m_ctx_head, EVP_sha1()); break; case DKIM::DKIM_A_SHA256: EVP_VerifyInit(&m_ctx_head, EVP_sha256()); break; } CanonicalizationHeader canonicalhead(sig.GetCanonModeHeader()); // add all headers to our cache (they will be pop of the end) std::map<std::string, Message::HeaderList> headerCache; for (Message::HeaderList::const_iterator hIter = m_msg.GetHeaders().begin(); hIter != m_msg.GetHeaders().end(); ++hIter) { std::string headerName = (*hIter)->GetName(); transform(headerName.begin(), headerName.end(), headerName.begin(), tolower); headerCache[headerName].push_back(*hIter); } // add all signed headers to our hash for (std::list<std::string>::const_iterator hIter = sig.GetSignedHeaders().begin(); hIter != sig.GetSignedHeaders().end(); ++hIter) { std::string tmp; std::string name = *hIter; transform(name.begin(), name.end(), name.begin(), tolower); std::map<std::string, Message::HeaderList>::iterator head = headerCache.find(name); // if this occurred // 1. we do not have a header of that name at all // 2. all headers with that name has been included... if (head == headerCache.end() || head->second.size() == 0) continue; #ifdef DEBUG printf("[%s]\n", canonicalhead.FilterHeader(head->second.back()->GetHeader()).c_str()); printf("[CRLF]\n"); #endif tmp = canonicalhead.FilterHeader(head->second.back()->GetHeader()) + "\r\n"; head->second.pop_back(); EVP_VerifyUpdate(&m_ctx_head, tmp.c_str(), tmp.size()); } // add our dkim-signature to the calculation (remove the "b"-tag) std::string h = (*headerIter)->GetHeader().substr(0, (*headerIter)->GetValueOffset()); std::string v = (*headerIter)->GetHeader().substr((*headerIter)->GetValueOffset()); DKIM::TagListEntry bTag; sig.GetTag("b", bTag); v.erase((int)bTag.GetValueOffset(), bTag.GetValue().size()); std::string tmp = canonicalhead.FilterHeader(h + v); #ifdef DEBUG printf("[%s]\n", tmp.c_str()); #endif EVP_VerifyUpdate(&m_ctx_head, tmp.c_str(), tmp.size()); // verify the header signature if (EVP_VerifyFinal(&m_ctx_head, (const unsigned char*)sig.GetSignatureData().c_str(), sig.GetSignatureData().size(), pub.GetPublicKey() ) != 1) throw DKIM::PermanentError("Signature did not verify"); EVP_MD_CTX_cleanup(&m_ctx_head); // success! }
int main(int argc, char* argv[]) { __progname = argv[0]; bool validate = false; bool doubleDots = false; std::string selector; std::string domain; std::string keyfile; // no arguments if (argc < 2) usage(stderr, 2); // longopts static struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { "doubledots", no_argument, NULL, 'D' }, { "validate", no_argument, NULL, 'v' }, { "selector", required_argument, NULL, 's' }, { "domain", required_argument, NULL, 'd' }, { "keyfile", required_argument, NULL, 'k' }, { NULL, 0, NULL, 0 } }; // fetching arguments.. opterr = 0; optind = 0; int ch; while ((ch = getopt_long(argc, argv, "hvs:d:k:D", longopts, NULL)) != -1) { switch (ch) { case 'D': doubleDots = true; break; case 'v': validate = true; break; case 'h': usage(stdout, 0); break; case 's': selector = optarg; break; case 'd': domain = optarg; break; case 'k': keyfile = optarg; break; case 0: break; default: usage(stderr, 2); break; } } argc -= optind; argv += optind; if (argc < 1) usage(stderr, 2); if (!validate && (selector.empty() || domain.empty() || keyfile.empty())) usage(stderr, 2); // sign the message.. if (!validate) { // readkey std::ifstream kfp(keyfile); if (!kfp) { fprintf(stderr, "keyfile %s could not be open\n", keyfile.c_str()); return 1; } std::string key((std::istreambuf_iterator<char>(kfp)), std::istreambuf_iterator<char>()); std::ifstream fp(argv[0]); try { printf("%s\r\n", Signatory(fp, doubleDots).CreateSignature( SignatoryOptions() .SetPrivateKey(key) .SetDomain(domain) .SetSelector(selector) .SetCanonModeHeader(DKIM::DKIM_C_RELAXED) .SetCanonModeBody(DKIM::DKIM_C_RELAXED) ).c_str() ); } catch (std::runtime_error& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; } // validate messages (0 .. argv) for (int x = 0; x < argc; x++) { std::ifstream fp(argv[x]); Validatory mail(fp, doubleDots); mail.CustomDNSResolver = MyResolver; // first check ADSP status try { std::list<ADSP> adsp; mail.GetADSP(adsp); for (std::list<ADSP>::const_iterator i = adsp.begin(); i != adsp.end(); ++i) { printf("[%s][ADSP][%s] %s/%s\n", argv[x], i->GetDomain().c_str(), i->GetResultAsString().c_str(), i->GetReason().c_str()); } } catch (DKIM::TemporaryError& e) { printf("[%s][ADSP] TEMPERR:%s\n", argv[x], e.what()); } catch (DKIM::PermanentError& e) { printf("[%s][ADSP] PERMERR:%s\n", argv[x], e.what()); } // then list all valid SDID's for (Validatory::SignatureList::const_iterator i = mail.GetSignatures().begin(); i != mail.GetSignatures().end(); ++i) { DKIM::PublicKey pub; DKIM::Signature sig; try { mail.GetSignature(i, sig); mail.GetPublicKey(sig, pub); mail.CheckSignature(i, sig, pub); printf("[%s][%s] OK\n", argv[x], sig.GetDomain().c_str()); } catch (DKIM::TemporaryError& e) { printf("[%s][%s] TEMPERR:%s\n", argv[x], sig.GetDomain().c_str(), e.what()); } catch (DKIM::PermanentError& e) { if (pub.SoftFail()) printf("[%s][%s] SOFT:%s\n", argv[x], sig.GetDomain().c_str(), e.what()); else printf("[%s][%s] = %s\n", argv[x], sig.GetDomain().c_str(), e.what()); } } } return 0; }