File indexing completed on 2025-03-09 04:54:29

0001 /*
0002    SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "dkimchecksignaturejob.h"
0008 #include "dkimdownloadkeyjob.h"
0009 #include "dkiminfo.h"
0010 #include "dkimkeyrecord.h"
0011 #include "dkimmanagerkey.h"
0012 #include "dkimutil.h"
0013 #include "messageviewer_dkimcheckerdebug.h"
0014 
0015 #include <KEmailAddress>
0016 #include <QCryptographicHash>
0017 #include <QDateTime>
0018 #include <QFile>
0019 #include <QRegularExpression>
0020 #include <qca_publickey.h>
0021 
0022 // see https://tools.ietf.org/html/rfc6376
0023 // #define DEBUG_SIGNATURE_DKIM 1
0024 using namespace MessageViewer;
0025 DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent)
0026     : QObject(parent)
0027 {
0028 }
0029 
0030 DKIMCheckSignatureJob::~DKIMCheckSignatureJob() = default;
0031 
0032 MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckSignatureJob::createCheckResult() const
0033 {
0034     MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult result;
0035     result.error = mError;
0036     result.warning = mWarning;
0037     result.status = mStatus;
0038     result.sdid = mDkimInfo.domain();
0039     result.auid = mDkimInfo.agentOrUserIdentifier();
0040     result.fromEmail = mFromEmail;
0041     result.listSignatureAuthenticationResult = mCheckSignatureAuthenticationResult;
0042     return result;
0043 }
0044 
0045 QString DKIMCheckSignatureJob::bodyCanonizationResult() const
0046 {
0047     return mBodyCanonizationResult;
0048 }
0049 
0050 QString DKIMCheckSignatureJob::headerCanonizationResult() const
0051 {
0052     return mHeaderCanonizationResult;
0053 }
0054 
0055 void DKIMCheckSignatureJob::start()
0056 {
0057     if (!mMessage) {
0058         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Item has not a message";
0059         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0060         Q_EMIT result(createCheckResult());
0061         deleteLater();
0062         return;
0063     }
0064     if (auto hrd = mMessage->headerByType("DKIM-Signature")) {
0065         mDkimValue = hrd->asUnicodeString();
0066     }
0067     // Store mFromEmail before looking at mDkimValue value. Otherwise we can return a from empty
0068     if (auto hrd = mMessage->from(false)) {
0069         mFromEmail = KEmailAddress::extractEmailAddress(hrd->asUnicodeString());
0070     }
0071     if (mDkimValue.isEmpty()) {
0072         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned;
0073         Q_EMIT result(createCheckResult());
0074         deleteLater();
0075         return;
0076     }
0077     qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mFromEmail " << mFromEmail;
0078     if (!mDkimInfo.parseDKIM(mDkimValue)) {
0079         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse header" << mDkimValue;
0080         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0081         Q_EMIT result(createCheckResult());
0082         deleteLater();
0083         return;
0084     }
0085 
0086     const MessageViewer::DKIMCheckSignatureJob::DKIMStatus status = checkSignature(mDkimInfo);
0087     if (status != MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid) {
0088         mStatus = status;
0089         Q_EMIT result(createCheckResult());
0090         deleteLater();
0091         return;
0092     }
0093     // ComputeBodyHash now.
0094     switch (mDkimInfo.bodyCanonization()) {
0095     case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
0096         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyCanonicalization;
0097         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0098         Q_EMIT result(createCheckResult());
0099         deleteLater();
0100         return;
0101     case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
0102         mBodyCanonizationResult = bodyCanonizationSimple();
0103         break;
0104     case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
0105         mBodyCanonizationResult = bodyCanonizationRelaxed();
0106         break;
0107     }
0108     // qDebug() << " bodyCanonizationResult "<< mBodyCanonizationResult << " algorithm " << mDkimInfo.hashingAlgorithm() << mDkimInfo.bodyHash();
0109 
0110     if (mDkimInfo.bodyLengthCount() != -1) { // Verify it.
0111         if (mDkimInfo.bodyLengthCount() > mBodyCanonizationResult.length()) {
0112             // length tag exceeds body size
0113             qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << " mDkimInfo.bodyLengthCount() " << mDkimInfo.bodyLengthCount() << " mBodyCanonizationResult.length() "
0114                                                    << mBodyCanonizationResult.length();
0115             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::SignatureTooLarge;
0116             mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0117             Q_EMIT result(createCheckResult());
0118             deleteLater();
0119             return;
0120         } else if (mDkimInfo.bodyLengthCount() < mBodyCanonizationResult.length()) {
0121             mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::SignatureTooSmall;
0122         }
0123         // truncated body to the length specified in the "l=" tag
0124         mBodyCanonizationResult = mBodyCanonizationResult.left(mDkimInfo.bodyLengthCount());
0125     }
0126     if (mBodyCanonizationResult.startsWith(QLatin1StringView("\r\n"))) { // Remove it from start
0127         mBodyCanonizationResult = mBodyCanonizationResult.right(mBodyCanonizationResult.length() - 2);
0128     }
0129     // It seems that kmail add a space before this line => it breaks check
0130     if (mBodyCanonizationResult.startsWith(QLatin1StringView(" This is a multi-part message in MIME format"))) { // Remove it from start
0131         mBodyCanonizationResult.replace(QStringLiteral(" This is a multi-part message in MIME format"),
0132                                         QStringLiteral("This is a multi-part message in MIME format"));
0133     }
0134     // It seems that kmail add a space before this line => it breaks check
0135     if (mBodyCanonizationResult.startsWith(QLatin1StringView(" This is a cryptographically signed message in MIME format."))) { // Remove it from start
0136         mBodyCanonizationResult.replace(QStringLiteral(" This is a cryptographically signed message in MIME format."),
0137                                         QStringLiteral("This is a cryptographically signed message in MIME format."));
0138     }
0139     if (mBodyCanonizationResult.startsWith(QLatin1StringView(" \r\n"))) { // Remove it from start
0140         static const QRegularExpression reg{QStringLiteral("^ \r\n")};
0141         mBodyCanonizationResult.remove(reg);
0142     }
0143 #ifdef DEBUG_SIGNATURE_DKIM
0144     QFile caFile(QStringLiteral("/tmp/bodycanon-kmail.txt"));
0145     caFile.open(QIODevice::WriteOnly | QIODevice::Text);
0146     QTextStream outStream(&caFile);
0147     outStream << mBodyCanonizationResult;
0148     caFile.close();
0149 #endif
0150 
0151     QByteArray resultHash;
0152     switch (mDkimInfo.hashingAlgorithm()) {
0153     case DKIMInfo::HashingAlgorithmType::Sha1:
0154         resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha1);
0155         break;
0156     case DKIMInfo::HashingAlgorithmType::Sha256:
0157         resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha256);
0158         break;
0159     case DKIMInfo::HashingAlgorithmType::Any:
0160     case DKIMInfo::HashingAlgorithmType::Unknown:
0161         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InsupportedHashAlgorithm;
0162         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0163         Q_EMIT result(createCheckResult());
0164         deleteLater();
0165         return;
0166     }
0167 
0168     // compare body hash
0169     qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "resultHash " << resultHash << "mDkimInfo.bodyHash()" << mDkimInfo.bodyHash();
0170     if (resultHash != mDkimInfo.bodyHash().toLatin1()) {
0171         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Corrupted body hash";
0172         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::CorruptedBodyHash;
0173         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0174         Q_EMIT result(createCheckResult());
0175         deleteLater();
0176         return;
0177     }
0178 
0179     if (mDkimInfo.headerCanonization() == MessageViewer::DKIMInfo::CanonicalizationType::Unknown) {
0180         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidHeaderCanonicalization;
0181         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0182         Q_EMIT result(createCheckResult());
0183         deleteLater();
0184         return;
0185     }
0186     // Parse message header
0187     if (!mHeaderParser.wasAlreadyParsed()) {
0188         mHeaderParser.setHead(mMessage->head());
0189         mHeaderParser.parse();
0190     }
0191 
0192     computeHeaderCanonization(true);
0193     if (mPolicy.saveKey() == MessageViewer::MessageViewerSettings::EnumSaveKey::Save) {
0194         const QString keyValue = MessageViewer::DKIMManagerKey::self()->keyValue(mDkimInfo.selector(), mDkimInfo.domain());
0195         // qDebug() << " mDkimInfo.selector() " << mDkimInfo.selector() << "mDkimInfo.domain()  " << mDkimInfo.domain() << keyValue;
0196         if (keyValue.isEmpty()) {
0197             downloadKey(mDkimInfo);
0198         } else {
0199             parseDKIMKeyRecord(keyValue, mDkimInfo.domain(), mDkimInfo.selector(), false);
0200             MessageViewer::DKIMManagerKey::self()->updateLastUsed(mDkimInfo.domain(), mDkimInfo.selector());
0201         }
0202     } else {
0203         downloadKey(mDkimInfo);
0204     }
0205 }
0206 
0207 void DKIMCheckSignatureJob::computeHeaderCanonization(bool removeQuoteOnContentType)
0208 {
0209     // Compute Hash Header
0210     switch (mDkimInfo.headerCanonization()) {
0211     case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
0212         return;
0213     case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
0214         mHeaderCanonizationResult = headerCanonizationSimple();
0215         break;
0216     case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
0217         mHeaderCanonizationResult = headerCanonizationRelaxed(removeQuoteOnContentType);
0218         break;
0219     }
0220 
0221     //    In hash step 2, the Signer/Verifier MUST pass the following to the
0222     //       hash algorithm in the indicated order.
0223 
0224     //       1.  The header fields specified by the "h=" tag, in the order
0225     //           specified in that tag, and canonicalized using the header
0226     //           canonicalization algorithm specified in the "c=" tag.  Each
0227     //           header field MUST be terminated with a single CRLF.
0228 
0229     //       2.  The DKIM-Signature header field that exists (verifying) or will
0230     //           be inserted (signing) in the message, with the value of the "b="
0231     //           tag (including all surrounding whitespace) deleted (i.e., treated
0232     //           as the empty string), canonicalized using the header
0233     //           canonicalization algorithm specified in the "c=" tag, and without
0234     //           a trailing CRLF.
0235     // add DKIM-Signature header to the hash input
0236     // with the value of the "b=" tag (including all surrounding whitespace) deleted
0237 
0238     // Add dkim-signature as lowercase
0239 
0240     QString dkimValue = mDkimValue;
0241     dkimValue = dkimValue.left(dkimValue.indexOf(QLatin1StringView("b=")) + 2);
0242     switch (mDkimInfo.headerCanonization()) {
0243     case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
0244         return;
0245     case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
0246         mHeaderCanonizationResult += QLatin1StringView("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QStringLiteral("dkim-signature"), dkimValue);
0247         break;
0248     case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
0249         mHeaderCanonizationResult += QLatin1StringView("\r\n")
0250             + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QStringLiteral("dkim-signature"), dkimValue, removeQuoteOnContentType);
0251         break;
0252     }
0253 #ifdef DEBUG_SIGNATURE_DKIM
0254     QFile headerFile(
0255         QStringLiteral("/tmp/headercanon-kmail-%1.txt").arg(removeQuoteOnContentType ? QLatin1StringView("removequote") : QLatin1StringView("withquote")));
0256     headerFile.open(QIODevice::WriteOnly | QIODevice::Text);
0257     QTextStream outHeaderStream(&headerFile);
0258     outHeaderStream << mHeaderCanonizationResult;
0259     headerFile.close();
0260 #endif
0261 }
0262 
0263 void DKIMCheckSignatureJob::setHeaderParser(const DKIMHeaderParser &headerParser)
0264 {
0265     mHeaderParser = headerParser;
0266 }
0267 
0268 void DKIMCheckSignatureJob::setCheckSignatureAuthenticationResult(const QList<DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult> &lst)
0269 {
0270     mCheckSignatureAuthenticationResult = lst;
0271 }
0272 
0273 QString DKIMCheckSignatureJob::bodyCanonizationSimple() const
0274 {
0275     /*
0276      * canonicalize the body using the simple algorithm
0277      * specified in Section 3.4.3 of RFC 6376
0278      */
0279     //    The "simple" body canonicalization algorithm ignores all empty lines
0280     //       at the end of the message body.  An empty line is a line of zero
0281     //       length after removal of the line terminator.  If there is no body or
0282     //       no trailing CRLF on the message body, a CRLF is added.  It makes no
0283     //       other changes to the message body.  In more formal terms, the
0284     //       "simple" body canonicalization algorithm converts "*CRLF" at the end
0285     //       of the body to a single "CRLF".
0286 
0287     //       Note that a completely empty or missing body is canonicalized as a
0288     //       single "CRLF"; that is, the canonicalized length will be 2 octets.
0289 
0290     return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->encodedBody()));
0291 }
0292 
0293 QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const
0294 {
0295     /*
0296      * canonicalize the body using the relaxed algorithm
0297      * specified in Section 3.4.4 of RFC 6376
0298      */
0299     /*
0300         a.  Reduce whitespace:
0301 
0302             *  Ignore all whitespace at the end of lines.  Implementations
0303                 MUST NOT remove the CRLF at the end of the line.
0304 
0305             *  Reduce all sequences of WSP within a line to a single SP
0306                 character.
0307 
0308         b.  Ignore all empty lines at the end of the message body.  "Empty
0309             line" is defined in Section 3.4.3.  If the body is non-empty but
0310             does not end with a CRLF, a CRLF is added.  (For email, this is
0311             only possible when using extensions to SMTP or non-SMTP transport
0312             mechanisms.)
0313         */
0314     const QString returnValue = MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromLatin1(mMessage->encodedBody()));
0315     return returnValue;
0316 }
0317 
0318 QString DKIMCheckSignatureJob::headerCanonizationSimple() const
0319 {
0320     QString headers;
0321 
0322     DKIMHeaderParser parser = mHeaderParser;
0323 
0324     const auto listSignedHeader{mDkimInfo.listSignedHeader()};
0325     for (const QString &header : listSignedHeader) {
0326         const QString str = parser.headerType(header.toLower());
0327         if (!str.isEmpty()) {
0328             if (!headers.isEmpty()) {
0329                 headers += QLatin1StringView("\r\n");
0330             }
0331             headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, str);
0332         }
0333     }
0334     return headers;
0335 }
0336 
0337 QString DKIMCheckSignatureJob::headerCanonizationRelaxed(bool removeQuoteOnContentType) const
0338 {
0339     //    The "relaxed" header canonicalization algorithm MUST apply the
0340     //       following steps in order:
0341 
0342     //       o  Convert all header field names (not the header field values) to
0343     //          lowercase.  For example, convert "SUBJect: AbC" to "subject: AbC".
0344 
0345     //       o  Unfold all header field continuation lines as described in
0346     //          [RFC5322]; in particular, lines with terminators embedded in
0347     //          continued header field values (that is, CRLF sequences followed by
0348     //          WSP) MUST be interpreted without the CRLF.  Implementations MUST
0349     //          NOT remove the CRLF at the end of the header field value.
0350 
0351     //       o  Convert all sequences of one or more WSP characters to a single SP
0352     //          character.  WSP characters here include those before and after a
0353     //          line folding boundary.
0354 
0355     //       o  Delete all WSP characters at the end of each unfolded header field
0356     //          value.
0357 
0358     //       o  Delete any WSP characters remaining before and after the colon
0359     //          separating the header field name from the header field value.  The
0360     //          colon separator MUST be retained.
0361 
0362     QString headers;
0363     DKIMHeaderParser parser = mHeaderParser;
0364     const auto listSignedHeader = mDkimInfo.listSignedHeader();
0365     for (const QString &header : listSignedHeader) {
0366         const QString str = parser.headerType(header.toLower());
0367         if (!str.isEmpty()) {
0368             if (!headers.isEmpty()) {
0369                 headers += QLatin1StringView("\r\n");
0370             }
0371             headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, str, removeQuoteOnContentType);
0372         }
0373     }
0374     return headers;
0375 }
0376 
0377 void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info)
0378 {
0379     auto job = new DKIMDownloadKeyJob(this);
0380     job->setDomainName(info.domain());
0381     job->setSelectorName(info.selector());
0382     connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) {
0383         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString;
0384         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
0385         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0386         Q_EMIT result(createCheckResult());
0387         deleteLater();
0388     });
0389     connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone);
0390 
0391     if (!job->start()) {
0392         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey";
0393         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
0394         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0395         Q_EMIT result(createCheckResult());
0396         deleteLater();
0397     }
0398 }
0399 
0400 void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList<QByteArray> &lst, const QString &domain, const QString &selector)
0401 {
0402     QByteArray ba;
0403     if (lst.count() != 1) {
0404         for (const QByteArray &b : lst) {
0405             ba += b;
0406         }
0407         // qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst;
0408     } else {
0409         ba = lst.at(0);
0410     }
0411     parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true);
0412 }
0413 
0414 void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue)
0415 {
0416     qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG)
0417         << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) key:" << str;
0418     if (!mDkimKeyRecord.parseKey(str)) {
0419         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str;
0420         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0421         Q_EMIT result(createCheckResult());
0422         deleteLater();
0423         return;
0424     }
0425     const QString keyType{mDkimKeyRecord.keyType()};
0426     if ((keyType != QLatin1StringView("rsa")) && (keyType != QLatin1StringView("ed25519"))) {
0427         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << keyType << " str " << str;
0428         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0429         Q_EMIT result(createCheckResult());
0430         deleteLater();
0431         return;
0432     }
0433 
0434     // if s flag is set in DKIM key record
0435     // AUID must be from the same domain as SDID (and not a subdomain)
0436     if (mDkimKeyRecord.flags().contains(QLatin1StringView("s"))) {
0437         //                  s  Any DKIM-Signature header fields using the "i=" tag MUST have
0438         //                     the same domain value on the right-hand side of the "@" in the
0439         //                     "i=" tag and the value of the "d=" tag.  That is, the "i="
0440         //                     domain MUST NOT be a subdomain of "d=".  Use of this flag is
0441         //                     RECOMMENDED unless subdomaining is required.
0442         if (mDkimInfo.iDomain() != mDkimInfo.domain()) {
0443             mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0444             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI;
0445             Q_EMIT result(createCheckResult());
0446             deleteLater();
0447             return;
0448         }
0449     }
0450     // TODO add support for ed25119
0451 
0452     // check that the testing flag is not set
0453     if (mDkimKeyRecord.flags().contains(QLatin1StringView("y"))) {
0454         if (!mPolicy.verifySignatureWhenOnlyTest()) {
0455             qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!";
0456             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode;
0457             mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0458             Q_EMIT result(createCheckResult());
0459             deleteLater();
0460             return;
0461         }
0462     }
0463     if (mDkimKeyRecord.publicKey().isEmpty()) {
0464         // empty value means that this public key has been revoked
0465         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked ";
0466         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked;
0467         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0468         Q_EMIT result(createCheckResult());
0469         deleteLater();
0470         return;
0471     }
0472 
0473     if (storeKeyValue) {
0474         Q_EMIT storeKey(str, domain, selector);
0475     }
0476 
0477     verifySignature();
0478 }
0479 
0480 void DKIMCheckSignatureJob::verifySignature()
0481 {
0482     const QString keyType{mDkimKeyRecord.keyType()};
0483     if (keyType == QLatin1StringView("rsa")) {
0484         verifyRSASignature();
0485     } else if (keyType == QLatin1StringView("ed25519")) {
0486         verifyEd25519Signature();
0487     } else {
0488         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " It's a bug " << keyType;
0489     }
0490 }
0491 
0492 void DKIMCheckSignatureJob::verifyEd25519Signature()
0493 {
0494     // TODO implement it.
0495     qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "it's a Ed25519 signed email";
0496     mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
0497     mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0498     Q_EMIT result(createCheckResult());
0499     deleteLater();
0500 }
0501 
0502 void DKIMCheckSignatureJob::verifyRSASignature()
0503 {
0504     QCA::ConvertResult conversionResult;
0505     // qDebug() << "mDkimKeyRecord.publicKey() " <<mDkimKeyRecord.publicKey() << " QCA::base64ToArray(mDkimKeyRecord.publicKey() "
0506     // <<QCA::base64ToArray(mDkimKeyRecord.publicKey());
0507     QCA::PublicKey publicKey = QCA::RSAPublicKey::fromDER(QCA::base64ToArray(mDkimKeyRecord.publicKey()), &conversionResult);
0508     if (QCA::ConvertGood != conversionResult) {
0509         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Public key read failed" << conversionResult << " public key" << mDkimKeyRecord.publicKey();
0510         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
0511         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0512         Q_EMIT result(createCheckResult());
0513         deleteLater();
0514         return;
0515     } else {
0516         qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Success loading public key";
0517     }
0518     QCA::RSAPublicKey rsaPublicKey = publicKey.toRSA();
0519     // qDebug() << "publicKey.modulus" << rsaPublicKey.n().toString();
0520     // qDebug() << "publicKey.exponent" << rsaPublicKey.e().toString();
0521 
0522     if (rsaPublicKey.e().toString().toLong() * 4 < 1024) {
0523         const int publicRsaTooSmallPolicyValue = mPolicy.publicRsaTooSmallPolicy();
0524         if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Nothing) {
0525             // Nothing
0526         } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Warning) {
0527             mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::PublicRsaKeyTooSmall;
0528         } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Error) {
0529             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyTooSmall;
0530             mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0531             Q_EMIT result(createCheckResult());
0532             deleteLater();
0533             return;
0534         }
0535 
0536     } else if (rsaPublicKey.e().toString().toLong() * 4 < 2048) {
0537         // TODO
0538     }
0539     // qDebug() << "mHeaderCanonizationResult " << mHeaderCanonizationResult << " mDkimInfo.signature() " << mDkimInfo.signature();
0540     if (rsaPublicKey.canVerify()) {
0541         const QString s = mDkimInfo.signature().remove(QLatin1Char(' '));
0542         QCA::SecureArray sec = mHeaderCanonizationResult.toLatin1();
0543         const QByteArray ba = QCA::base64ToArray(s);
0544         // qDebug() << " s base ba" << ba;
0545         QCA::SignatureAlgorithm sigAlg;
0546         switch (mDkimInfo.hashingAlgorithm()) {
0547         case DKIMInfo::HashingAlgorithmType::Sha1:
0548             sigAlg = QCA::EMSA3_SHA1;
0549             break;
0550         case DKIMInfo::HashingAlgorithmType::Sha256:
0551             sigAlg = QCA::EMSA3_SHA256;
0552             break;
0553         case DKIMInfo::HashingAlgorithmType::Any:
0554         case DKIMInfo::HashingAlgorithmType::Unknown: {
0555             // then signature is invalid
0556             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
0557             mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0558             Q_EMIT result(createCheckResult());
0559             deleteLater();
0560             qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "DKIMInfo::HashingAlgorithmType undefined ! ";
0561             return;
0562         }
0563         }
0564         if (!rsaPublicKey.verifyMessage(sec, ba, sigAlg, QCA::DERSequence)) {
0565             computeHeaderCanonization(false);
0566             const QCA::SecureArray secWithoutQuote = mHeaderCanonizationResult.toLatin1();
0567             if (!rsaPublicKey.verifyMessage(secWithoutQuote, ba, sigAlg, QCA::DERSequence)) {
0568                 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature invalid";
0569                 // then signature is invalid
0570                 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
0571                 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0572                 Q_EMIT result(createCheckResult());
0573                 deleteLater();
0574                 return;
0575             }
0576         }
0577     } else {
0578         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to verify signature";
0579         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
0580         mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0581         Q_EMIT result(createCheckResult());
0582         deleteLater();
0583         return;
0584     }
0585     mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
0586     Q_EMIT result(createCheckResult());
0587     deleteLater();
0588 }
0589 
0590 DKIMCheckPolicy DKIMCheckSignatureJob::policy() const
0591 {
0592     return mPolicy;
0593 }
0594 
0595 void DKIMCheckSignatureJob::setPolicy(const DKIMCheckPolicy &policy)
0596 {
0597     mPolicy = policy;
0598 }
0599 
0600 DKIMCheckSignatureJob::DKIMWarning DKIMCheckSignatureJob::warning() const
0601 {
0602     return mWarning;
0603 }
0604 
0605 void DKIMCheckSignatureJob::setWarning(DKIMCheckSignatureJob::DKIMWarning warning)
0606 {
0607     mWarning = warning;
0608 }
0609 
0610 KMime::Message::Ptr DKIMCheckSignatureJob::message() const
0611 {
0612     return mMessage;
0613 }
0614 
0615 void DKIMCheckSignatureJob::setMessage(const KMime::Message::Ptr &message)
0616 {
0617     mMessage = message;
0618 }
0619 
0620 MessageViewer::DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::checkSignature(const DKIMInfo &info)
0621 {
0622     const qint64 currentDate = QDateTime::currentSecsSinceEpoch();
0623     if (info.expireTime() != -1 && info.expireTime() < currentDate) {
0624         mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureExpired;
0625     }
0626     if (info.signatureTimeStamp() != -1 && info.signatureTimeStamp() > currentDate) {
0627         mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture;
0628     }
0629     if (info.signature().isEmpty()) {
0630         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist";
0631         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature;
0632         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0633     }
0634     if (!info.listSignedHeader().contains(QLatin1StringView("from"), Qt::CaseInsensitive)) {
0635         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list";
0636         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom;
0637         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0638     }
0639     if (info.domain().isEmpty()) {
0640         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined.";
0641         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist;
0642         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0643     }
0644     if (info.query() != QLatin1StringView("dns/txt")) {
0645         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query();
0646         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod;
0647         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0648     }
0649 
0650     if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any)
0651         || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) {
0652         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty";
0653         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm;
0654         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0655     }
0656     if (info.signingAlgorithm().isEmpty()) {
0657         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty";
0658         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm;
0659         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0660     }
0661 
0662     if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) {
0663         if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) {
0664             // nothing
0665         } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) {
0666             qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error";
0667             mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe;
0668         } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) {
0669             qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error";
0670             mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1;
0671             return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0672         }
0673     }
0674 
0675     // qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain();
0676     if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) {
0677         qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID";
0678         mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError;
0679         return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
0680     }
0681     // Add more test
0682     // TODO check if info is valid
0683     return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
0684 }
0685 
0686 DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const
0687 {
0688     return mError;
0689 }
0690 
0691 DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const
0692 {
0693     return mStatus;
0694 }
0695 
0696 void DKIMCheckSignatureJob::setStatus(DKIMCheckSignatureJob::DKIMStatus status)
0697 {
0698     mStatus = status;
0699 }
0700 
0701 QString DKIMCheckSignatureJob::dkimValue() const
0702 {
0703     return mDkimValue;
0704 }
0705 
0706 bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const
0707 {
0708     return status != DKIMCheckSignatureJob::DKIMStatus::Unknown;
0709 }
0710 
0711 bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
0712 {
0713     return error == other.error && warning == other.warning && status == other.status && fromEmail == other.fromEmail && auid == other.auid
0714         && sdid == other.sdid && listSignatureAuthenticationResult == other.listSignatureAuthenticationResult;
0715 }
0716 
0717 bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
0718 {
0719     return !CheckSignatureResult::operator==(other);
0720 }
0721 
0722 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::CheckSignatureResult &t)
0723 {
0724     d << " error " << t.error;
0725     d << " warning " << t.warning;
0726     d << " status " << t.status;
0727     d << " signedBy " << t.sdid;
0728     d << " fromEmail " << t.fromEmail;
0729     d << " auid " << t.auid;
0730     d << " authenticationResult " << t.listSignatureAuthenticationResult;
0731     return d;
0732 }
0733 
0734 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &t)
0735 {
0736     d << " method " << t.method;
0737     d << " errorStr " << t.errorStr;
0738     d << " status " << t.status;
0739     d << " sdid " << t.sdid;
0740     d << " auid " << t.auid;
0741     d << " inforesult " << t.infoResult;
0742     return d;
0743 }
0744 
0745 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::operator==(const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &other) const
0746 {
0747     return errorStr == other.errorStr && method == other.method && status == other.status && sdid == other.sdid && auid == other.auid
0748         && infoResult == other.infoResult;
0749 }
0750 
0751 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::isValid() const
0752 {
0753     // TODO improve it
0754     return (method != AuthenticationMethod::Unknown);
0755 }
0756 
0757 #include "moc_dkimchecksignaturejob.cpp"