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"