diff --git a/messageviewer/src/dkim-verify/autotests/dkimauthenticationstatusinfotest.cpp b/messageviewer/src/dkim-verify/autotests/dkimauthenticationstatusinfotest.cpp index 6041825a..ada9de3f 100644 --- a/messageviewer/src/dkim-verify/autotests/dkimauthenticationstatusinfotest.cpp +++ b/messageviewer/src/dkim-verify/autotests/dkimauthenticationstatusinfotest.cpp @@ -1,80 +1,89 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkimauthenticationstatusinfotest.h" #include "dkim-verify/dkimauthenticationstatusinfo.h" #include QTEST_GUILESS_MAIN(DKIMAuthenticationStatusInfoTest) DKIMAuthenticationStatusInfoTest::DKIMAuthenticationStatusInfoTest(QObject *parent) : QObject(parent) { } void DKIMAuthenticationStatusInfoTest::shouldHaveDefaultValue() { MessageViewer::DKIMAuthenticationStatusInfo info; QVERIFY(info.authservId().isEmpty()); QCOMPARE(info.authVersion(), -1); QVERIFY(info.reasonSpec().isEmpty()); QVERIFY(info.listAuthStatusInfo().isEmpty()); } void DKIMAuthenticationStatusInfoTest::shouldParseKey() { QFETCH(QString, key); QFETCH(MessageViewer::DKIMAuthenticationStatusInfo, result); QFETCH(bool, success); MessageViewer::DKIMAuthenticationStatusInfo info; const bool val = info.parseAuthenticationStatus(key); QCOMPARE(val, success); const bool compareResult = result == info; if (!compareResult) { qDebug() << "parse info " << info; qDebug() << "expected " << result; } QVERIFY(compareResult); } void DKIMAuthenticationStatusInfoTest::shouldParseKey_data() { QTest::addColumn("key"); QTest::addColumn("result"); QTest::addColumn("success"); QTest::addRow("empty") << QString() << MessageViewer::DKIMAuthenticationStatusInfo() << false; { MessageViewer::DKIMAuthenticationStatusInfo info; info.setAuthVersion(1); info.setAuthservId(QStringLiteral("in68.mail.ovh.net")); QTest::addRow("test1") << QStringLiteral("in68.mail.ovh.net; dkim=pass (2048-bit key; unprotected) header.d=kde.org header.i=@kde.org header.b=\"GMG2ucPx\"; dkim=pass (2048-bit key; unprotected) header.d=kde.org header.i=@kde.org header.b=\"I3t3p7Up\"; dkim-atps=neutral") << info << true; } { MessageViewer::DKIMAuthenticationStatusInfo info; info.setAuthVersion(1); info.setAuthservId(QStringLiteral("example.org")); QTest::addRow("none") << QStringLiteral("example.org 1; none") << info << false; } + { + MessageViewer::DKIMAuthenticationStatusInfo info; + info.setAuthVersion(1); + info.setAuthservId(QStringLiteral("example.org")); + QTest::addRow("reason") << QStringLiteral("example.com; dkim=pass reason=\"good signature\" header.i=@mail-router.example.net; dkim=fail reason=\"bad signature\" header.i=@newyork.example.com") + << info + << true; + + } } diff --git a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp index 471ffd42..c9a8f3af 100644 --- a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp +++ b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp @@ -1,194 +1,208 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkimauthenticationstatusinfo.h" #include "dkimauthenticationstatusinfoutil.h" #include "messageviewer_dkimcheckerdebug.h" #include using namespace MessageViewer; //see https://tools.ietf.org/html/rfc7601 DKIMAuthenticationStatusInfo::DKIMAuthenticationStatusInfo() { } bool DKIMAuthenticationStatusInfo::parseAuthenticationStatus(const QString &key) { QString valueKey = key; // https://tools.ietf.org/html/rfc7601#section-2.2 // authres-header = "Authentication-Results:" [CFWS] authserv-id // [ CFWS authres-version ] // ( no-result / 1*resinfo ) [CFWS] CRLF // 1) extract AuthservId and AuthVersion QRegularExpressionMatch match; const QString regStr = DKIMAuthenticationStatusInfoUtil::value_cp() + QLatin1String("(?:") + DKIMAuthenticationStatusInfoUtil::cfws_p() + QLatin1String("([0-9]+)") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1String(" )?"); //qDebug() << " regStr" << regStr; int index = valueKey.indexOf(QRegularExpression(regStr), 0, &match); if (index != -1) { mAuthservId = match.captured(1); if (!match.captured(2).isEmpty()) { mAuthVersion = match.captured(2).toInt(); } else { mAuthVersion = 1; } valueKey = valueKey.right(valueKey.length() - (index + match.captured(0).length())); qDebug() << " match.captured(0)"< DKIMAuthenticationStatusInfo::listAuthStatusInfo() const { return mListAuthStatusInfo; } void DKIMAuthenticationStatusInfo::setListAuthStatusInfo(const QList &listAuthStatusInfo) { mListAuthStatusInfo = listAuthStatusInfo; } QString DKIMAuthenticationStatusInfo::authservId() const { return mAuthservId; } void DKIMAuthenticationStatusInfo::setAuthservId(const QString &authservId) { mAuthservId = authservId; } QDebug operator <<(QDebug d, const DKIMAuthenticationStatusInfo &t) { d << "mAuthservId: " << t.authservId(); d << "mReasonSpec: " << t.reasonSpec(); d << "mAuthVersion: " << t.authVersion(); for (const DKIMAuthenticationStatusInfo::AuthStatusInfo & info : t.listAuthStatusInfo()) { d << "mListAuthStatusInfo: " << info.method << " : " << info.result << " : " << info.methodVersion << " : " << info.reason; } return d; } bool DKIMAuthenticationStatusInfo::AuthStatusInfo::operator==(const DKIMAuthenticationStatusInfo::AuthStatusInfo &other) const { return other.method == method && other.result == result && other.methodVersion == methodVersion && other.reason == reason; } bool DKIMAuthenticationStatusInfo::AuthStatusInfo::isValid() const { return !method.isEmpty(); } diff --git a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp index c7c8ea62..89e4b7cb 100644 --- a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp +++ b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp @@ -1,657 +1,676 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkimchecksignaturejob.h" #include "dkimdownloadkeyjob.h" #include "dkimmanagerkey.h" #include "dkiminfo.h" #include "dkimutil.h" #include "dkimkeyrecord.h" #include "messageviewer_dkimcheckerdebug.h" #include "dkimheaderparser.h" #include #include #include #include #include //see https://tools.ietf.org/html/rfc6376 //#define DEBUG_SIGNATURE_DKIM 1 using namespace MessageViewer; DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent) : QObject(parent) { } DKIMCheckSignatureJob::~DKIMCheckSignatureJob() { } MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckSignatureJob::createCheckResult() { MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult result; result.error = mError; result.warning = mWarning; result.status = mStatus; result.item = mMessageItem; result.signedBy = mDkimInfo.domain(); result.fromEmail = mFromEmail; return result; } QString DKIMCheckSignatureJob::bodyCanonizationResult() const { return mBodyCanonizationResult; } QString DKIMCheckSignatureJob::headerCanonizationResult() const { return mHeaderCanonizationResult; } void DKIMCheckSignatureJob::start() { if (mMessageItem.isValid() && !mMessage) { if (mMessageItem.hasPayload()) { mMessage = mMessageItem.payload(); } } else if (mMessage) { //Nothing } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Item has not a message"; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (auto hrd = mMessage->headerByType("DKIM-Signature")) { mDkimValue = hrd->asUnicodeString(); } //Store mFromEmail before looking at mDkimValue value. Otherwise we can return a from empty if (auto hrd = mMessage->from(false)) { mFromEmail = KEmailAddress::extractEmailAddress(hrd->asUnicodeString()); } if (mDkimValue.isEmpty()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned; Q_EMIT result(createCheckResult()); deleteLater(); return; } qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mFromEmail " << mFromEmail; if (!mDkimInfo.parseDKIM(mDkimValue)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse header" << mDkimValue; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } const MessageViewer::DKIMCheckSignatureJob::DKIMStatus status = checkSignature(mDkimInfo); if (status != MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid) { mStatus = status; Q_EMIT result(createCheckResult()); deleteLater(); return; } //ComputeBodyHash now. switch (mDkimInfo.bodyCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mBodyCanonizationResult = bodyCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mBodyCanonizationResult = bodyCanonizationRelaxed(); break; } //qDebug() << " bodyCanonizationResult "<< mBodyCanonizationResult << " algorithm " << mDkimInfo.hashingAlgorithm() << mDkimInfo.bodyHash(); if (mDkimInfo.bodyLengthCount() != -1) { //Verify it. if (mDkimInfo.bodyLengthCount() < mBodyCanonizationResult.length()) { // length tag exceeds body size mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::SignatureTooLarge; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } else if (mDkimInfo.bodyLengthCount() > mBodyCanonizationResult.length()) { mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::SignatureTooSmall; } // truncated body to the length specified in the "l=" tag mBodyCanonizationResult = mBodyCanonizationResult.left(mDkimInfo.bodyLengthCount()); } if (mBodyCanonizationResult.startsWith(QLatin1String("\r\n"))) { //Remove it from start mBodyCanonizationResult = mBodyCanonizationResult.right(mBodyCanonizationResult.length() -2); } #ifdef DEBUG_SIGNATURE_DKIM QFile caFile(QStringLiteral("/tmp/bodycanon-kmail.txt")); caFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream outStream(&caFile); outStream << mBodyCanonizationResult; caFile.close(); #endif QByteArray resultHash; switch (mDkimInfo.hashingAlgorithm()) { case DKIMInfo::HashingAlgorithmType::Sha1: resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha1); break; case DKIMInfo::HashingAlgorithmType::Sha256: resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha256); break; case DKIMInfo::HashingAlgorithmType::Any: case DKIMInfo::HashingAlgorithmType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InsupportedHashAlgorithm; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // compare body hash qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "resultHash " << resultHash << "mDkimInfo.bodyHash()" << mDkimInfo.bodyHash(); if (resultHash != mDkimInfo.bodyHash().toLatin1()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Corrupted body hash"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::CorruptedBodyHash; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mDkimInfo.headerCanonization() == MessageViewer::DKIMInfo::CanonicalizationType::Unknown) { mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidHeaderCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } computeHeaderCanonization(true); if (mPolicy.saveKey()) { const QString keyValue = MessageViewer::DKIMManagerKey::self()->keyValue(mDkimInfo.selector(), mDkimInfo.domain()); //qDebug() << " mDkimInfo.selector() " << mDkimInfo.selector() << "mDkimInfo.domain() " << mDkimInfo.domain() << keyValue; if (keyValue.isEmpty()) { downloadKey(mDkimInfo); } else { parseDKIMKeyRecord(keyValue, mDkimInfo.domain(), mDkimInfo.selector(), false); } } else { downloadKey(mDkimInfo); } } void DKIMCheckSignatureJob::computeHeaderCanonization(bool removeQuoteOnContentType) { //Compute Hash Header switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult = headerCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult = headerCanonizationRelaxed(removeQuoteOnContentType); break; } // In hash step 2, the Signer/Verifier MUST pass the following to the // hash algorithm in the indicated order. // 1. The header fields specified by the "h=" tag, in the order // specified in that tag, and canonicalized using the header // canonicalization algorithm specified in the "c=" tag. Each // header field MUST be terminated with a single CRLF. // 2. The DKIM-Signature header field that exists (verifying) or will // be inserted (signing) in the message, with the value of the "b=" // tag (including all surrounding whitespace) deleted (i.e., treated // as the empty string), canonicalized using the header // canonicalization algorithm specified in the "c=" tag, and without // a trailing CRLF. // add DKIM-Signature header to the hash input // with the value of the "b=" tag (including all surrounding whitespace) deleted //Add dkim-signature as lowercase QString dkimValue = mDkimValue; dkimValue = dkimValue.left(dkimValue.indexOf(QLatin1String("b=")) + 2); switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QLatin1String("dkim-signature"), dkimValue); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QLatin1String("dkim-signature"), dkimValue, removeQuoteOnContentType); break; } #ifdef DEBUG_SIGNATURE_DKIM QFile headerFile(QStringLiteral("/tmp/headercanon-kmail-%1.txt").arg(removeQuoteOnContentType ? QLatin1String("removequote") : QLatin1String("withquote"))); headerFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream outHeaderStream(&headerFile); outHeaderStream << mHeaderCanonizationResult; headerFile.close(); #endif } QString DKIMCheckSignatureJob::bodyCanonizationSimple() const { /* * canonicalize the body using the simple algorithm * specified in Section 3.4.3 of RFC 6376 */ // The "simple" body canonicalization algorithm ignores all empty lines // at the end of the message body. An empty line is a line of zero // length after removal of the line terminator. If there is no body or // no trailing CRLF on the message body, a CRLF is added. It makes no // other changes to the message body. In more formal terms, the // "simple" body canonicalization algorithm converts "*CRLF" at the end // of the body to a single "CRLF". // Note that a completely empty or missing body is canonicalized as a // single "CRLF"; that is, the canonicalized length will be 2 octets. return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->encodedBody())); } QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const { /* * canonicalize the body using the relaxed algorithm * specified in Section 3.4.4 of RFC 6376 */ /* a. Reduce whitespace: * Ignore all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line. * Reduce all sequences of WSP within a line to a single SP character. b. Ignore all empty lines at the end of the message body. "Empty line" is defined in Section 3.4.3. If the body is non-empty but does not end with a CRLF, a CRLF is added. (For email, this is only possible when using extensions to SMTP or non-SMTP transport mechanisms.) */ const QString returnValue = MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromLatin1(mMessage->encodedBody())); return returnValue; } QString DKIMCheckSignatureJob::headerCanonizationSimple() const { QString headers; DKIMHeaderParser parser; parser.setHead(mMessage->head()); parser.parse(); for (const QString &header : mDkimInfo.listSignedHeader()) { const QString str = parser.headerType(header.toLower()); if (!str.isEmpty()) { if (!headers.isEmpty()) { headers += QLatin1String("\r\n"); } headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, str); } } return headers; } QString DKIMCheckSignatureJob::headerCanonizationRelaxed(bool removeQuoteOnContentType) const { // The "relaxed" header canonicalization algorithm MUST apply the // following steps in order: // o Convert all header field names (not the header field values) to // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". // o Unfold all header field continuation lines as described in // [RFC5322]; in particular, lines with terminators embedded in // continued header field values (that is, CRLF sequences followed by // WSP) MUST be interpreted without the CRLF. Implementations MUST // NOT remove the CRLF at the end of the header field value. // o Convert all sequences of one or more WSP characters to a single SP // character. WSP characters here include those before and after a // line folding boundary. // o Delete all WSP characters at the end of each unfolded header field // value. // o Delete any WSP characters remaining before and after the colon // separating the header field name from the header field value. The // colon separator MUST be retained. QString headers; DKIMHeaderParser parser; parser.setHead(mMessage->head()); parser.parse(); for (const QString &header : mDkimInfo.listSignedHeader()) { const QString str = parser.headerType(header.toLower()); if (!str.isEmpty()) { if (!headers.isEmpty()) { headers += QLatin1String("\r\n"); } headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, str, removeQuoteOnContentType); } } return headers; } void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info) { DKIMDownloadKeyJob *job = new DKIMDownloadKeyJob(this); job->setDomainName(info.domain()); job->setSelectorName(info.selector()); connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); }); connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone); if (!job->start()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); } } void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList &lst, const QString &domain, const QString &selector) { QByteArray ba; if (lst.count() != 1) { for (const QByteArray &b : lst) { ba += b; } qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst; } else { ba = lst.at(0); } parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true); } void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) { qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) key:" << str; if (!mDkimKeyRecord.parseKey(str)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mDkimKeyRecord.keyType() != QLatin1String("rsa")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << mDkimKeyRecord.keyType() << " str " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // if s flag is set in DKIM key record // AUID must be from the same domain as SDID (and not a subdomain) if (mDkimKeyRecord.flags().contains(QLatin1String("s"))) { // s Any DKIM-Signature header fields using the "i=" tag MUST have // the same domain value on the right-hand side of the "@" in the // "i=" tag and the value of the "d=" tag. That is, the "i=" // domain MUST NOT be a subdomain of "d=". Use of this flag is // RECOMMENDED unless subdomaining is required. if (mDkimInfo.iDomain() != mDkimInfo.domain()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI; Q_EMIT result(createCheckResult()); deleteLater(); return; } } // check that the testing flag is not set if (mDkimKeyRecord.flags().contains(QLatin1String("y"))) { if (!mPolicy.verifySignatureWhenOnlyTest()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } } if (mDkimKeyRecord.publicKey().isEmpty()) { // empty value means that this public key has been revoked qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked "; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (storeKeyValue) { Q_EMIT storeKey(str, domain, selector); } verifyRSASignature(); } void DKIMCheckSignatureJob::verifyRSASignature() { QCA::ConvertResult conversionResult; //qDebug() << "mDkimKeyRecord.publicKey() " < currentDate) { mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture; } if (info.signature().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (!info.listSignedHeader().contains(QLatin1String("from"), Qt::CaseInsensitive)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.domain().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined."; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.query() != QLatin1String("dns/txt")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query(); mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any) || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.signingAlgorithm().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) { if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) { //nothing } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error"; mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe; } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } } //qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain(); if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } //Add more test //TODO check if info is valid return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid; } DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const { return mError; } DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const { return mStatus; } void DKIMCheckSignatureJob::setStatus(const DKIMCheckSignatureJob::DKIMStatus &status) { mStatus = status; } QString DKIMCheckSignatureJob::dkimValue() const { return mDkimValue; } + +bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const +{ + return status != DKIMCheckSignatureJob::DKIMStatus::Unknown; +} + +bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const +{ + return error == other.error + && warning == other.warning + && status == other.status + && item == other.item + && fromEmail == other.fromEmail; +} + +bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const +{ + return !CheckSignatureResult::operator==(other); +} diff --git a/messageviewer/src/dkim-verify/dkimchecksignaturejob.h b/messageviewer/src/dkim-verify/dkimchecksignaturejob.h index 028ff951..546f6cc0 100644 --- a/messageviewer/src/dkim-verify/dkimchecksignaturejob.h +++ b/messageviewer/src/dkim-verify/dkimchecksignaturejob.h @@ -1,170 +1,157 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DKIMCHECKSIGNATUREJOB_H #define DKIMCHECKSIGNATUREJOB_H #include #include "messageviewer_export.h" #include #include #include #include #include namespace MessageViewer { /** * @brief The DKIMCheckSignatureJob class * @author Laurent Montel */ class MESSAGEVIEWER_EXPORT DKIMCheckSignatureJob : public QObject { Q_OBJECT public: enum class DKIMStatus : int { Unknown = 0, Valid = 1, Invalid = 2, EmailNotSigned = 3, NeedToBeSigned = 4 }; Q_ENUM(DKIMStatus) enum class DKIMError : int { Any = 0, CorruptedBodyHash = 1, DomainNotExist = 2, MissingFrom = 3, MissingSignature = 4, InvalidQueryMethod = 5, InvalidHeaderCanonicalization = 6, InvalidBodyCanonicalization = 7, InvalidBodyHashAlgorithm = 8, InvalidSignAlgorithm = 9, PublicKeyWasRevoked = 10, SignatureTooLarge = 11, InsupportedHashAlgorithm = 12, PublicKeyTooSmall = 13, ImpossibleToVerifySignature = 14, DomainI = 15, TestKeyMode = 16, ImpossibleToDownloadKey = 17, HashAlgorithmUnsafeSha1 = 18, IDomainError = 19, PublicKeyConversionError = 20, }; Q_ENUM(DKIMError) enum class DKIMWarning : int { Any = 0, SignatureExpired = 1, SignatureCreatedInFuture = 2, SignatureTooSmall = 3, HashAlgorithmUnsafe = 4, }; Q_ENUM(DKIMWarning) struct CheckSignatureResult { - bool isValid() const - { - return status != DKIMCheckSignatureJob::DKIMStatus::Unknown; - } - - Q_REQUIRED_RESULT bool operator==(const CheckSignatureResult &other) const - { - return error == other.error - && warning == other.warning - && status == other.status - && item == other.item - && fromEmail == other.fromEmail; - } - - Q_REQUIRED_RESULT bool operator!=(const CheckSignatureResult &other) const - { - return !CheckSignatureResult::operator==(other); - } + Q_REQUIRED_RESULT bool isValid() const; + + Q_REQUIRED_RESULT bool operator==(const CheckSignatureResult &other) const; + + Q_REQUIRED_RESULT bool operator!=(const CheckSignatureResult &other) const; DKIMCheckSignatureJob::DKIMError error = DKIMCheckSignatureJob::DKIMError::Any; DKIMCheckSignatureJob::DKIMWarning warning = DKIMCheckSignatureJob::DKIMWarning::Any; DKIMCheckSignatureJob::DKIMStatus status = DKIMCheckSignatureJob::DKIMStatus::Unknown; Akonadi::Item item; QString signedBy; QString fromEmail; }; explicit DKIMCheckSignatureJob(QObject *parent = nullptr); ~DKIMCheckSignatureJob(); void start(); Q_REQUIRED_RESULT QString dkimValue() const; Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMStatus status() const; void setStatus(const DKIMCheckSignatureJob::DKIMStatus &status); Q_REQUIRED_RESULT MessageViewer::DKIMCheckSignatureJob::DKIMStatus checkSignature(const MessageViewer::DKIMInfo &info); Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMError error() const; Q_REQUIRED_RESULT KMime::Message::Ptr message() const; void setMessage(const KMime::Message::Ptr &message); Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMWarning warning() const; void setWarning(const DKIMWarning &warning); Q_REQUIRED_RESULT QString headerCanonizationResult() const; Q_REQUIRED_RESULT QString bodyCanonizationResult() const; Q_REQUIRED_RESULT Akonadi::Item item() const; void setItem(const Akonadi::Item &item); Q_REQUIRED_RESULT DKIMCheckPolicy policy() const; void setPolicy(const DKIMCheckPolicy &policy); Q_SIGNALS: void result(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult); void storeKey(const QString &key, const QString &domain, const QString &selector); private: void downloadKey(const DKIMInfo &info); void slotDownloadKeyDone(const QList &lst, const QString &domain, const QString &selector); void parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue = true); Q_REQUIRED_RESULT QString headerCanonizationSimple() const; Q_REQUIRED_RESULT QString headerCanonizationRelaxed(bool removeQuoteOnContentType) const; Q_REQUIRED_RESULT QString bodyCanonizationRelaxed() const; Q_REQUIRED_RESULT QString bodyCanonizationSimple() const; Q_REQUIRED_RESULT MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult createCheckResult(); void verifyRSASignature(); void computeHeaderCanonization(bool removeQuoteOnContentType); DKIMCheckPolicy mPolicy; KMime::Message::Ptr mMessage; Akonadi::Item mMessageItem; QString mFromEmail; DKIMInfo mDkimInfo; DKIMKeyRecord mDkimKeyRecord; QString mDkimValue; QString mHeaderCanonizationResult; QString mBodyCanonizationResult; DKIMCheckSignatureJob::DKIMError mError = DKIMCheckSignatureJob::DKIMError::Any; DKIMCheckSignatureJob::DKIMWarning mWarning = DKIMCheckSignatureJob::DKIMWarning::Any; DKIMCheckSignatureJob::DKIMStatus mStatus = DKIMCheckSignatureJob::DKIMStatus::Unknown; }; } Q_DECLARE_METATYPE(MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult) #endif // DKIMCHECKSIGNATUREJOB_H