Messagelib

dkimchecksignaturejob.cpp
1/*
2 SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "dkimchecksignaturejob.h"
8#include "dkimdownloadkeyjob.h"
9#include "dkiminfo.h"
10#include "dkimkeyrecord.h"
11#include "dkimmanagerkey.h"
12#include "dkimutil.h"
13#include "messageviewer_dkimcheckerdebug.h"
14
15#include <KEmailAddress>
16#include <QCryptographicHash>
17#include <QDateTime>
18#include <QFile>
19#include <QRegularExpression>
20#include <qca_publickey.h>
21
22// see https://tools.ietf.org/html/rfc6376
23// #define DEBUG_SIGNATURE_DKIM 1
24using namespace MessageViewer;
25DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent)
26 : QObject(parent)
27{
28}
29
30DKIMCheckSignatureJob::~DKIMCheckSignatureJob() = default;
31
32MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckSignatureJob::createCheckResult() const
33{
34 MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult result;
35 result.error = mError;
36 result.warning = mWarning;
37 result.status = mStatus;
38 result.sdid = mDkimInfo.domain();
39 result.auid = mDkimInfo.agentOrUserIdentifier();
40 result.fromEmail = mFromEmail;
41 result.listSignatureAuthenticationResult = mCheckSignatureAuthenticationResult;
42 return result;
43}
44
45QString DKIMCheckSignatureJob::bodyCanonizationResult() const
46{
47 return mBodyCanonizationResult;
48}
49
50QString DKIMCheckSignatureJob::headerCanonizationResult() const
51{
52 return mHeaderCanonizationResult;
53}
54
55void DKIMCheckSignatureJob::start()
56{
57 if (!mMessage) {
58 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Item has not a message";
59 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
60 Q_EMIT result(createCheckResult());
62 return;
63 }
64 if (auto hrd = mMessage->headerByType("DKIM-Signature")) {
65 mDkimValue = hrd->asUnicodeString();
66 }
67 // Store mFromEmail before looking at mDkimValue value. Otherwise we can return a from empty
68 if (auto hrd = mMessage->from(false)) {
69 mFromEmail = KEmailAddress::extractEmailAddress(hrd->asUnicodeString());
70 }
71 if (mDkimValue.isEmpty()) {
72 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned;
73 Q_EMIT result(createCheckResult());
75 return;
76 }
77 qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mFromEmail " << mFromEmail;
78 if (!mDkimInfo.parseDKIM(mDkimValue)) {
79 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse header" << mDkimValue;
80 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
81 Q_EMIT result(createCheckResult());
83 return;
84 }
85
86 const MessageViewer::DKIMCheckSignatureJob::DKIMStatus status = checkSignature(mDkimInfo);
87 if (status != MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid) {
88 mStatus = status;
89 Q_EMIT result(createCheckResult());
91 return;
92 }
93 // ComputeBodyHash now.
94 switch (mDkimInfo.bodyCanonization()) {
95 case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
96 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyCanonicalization;
97 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
98 Q_EMIT result(createCheckResult());
100 return;
101 case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
102 mBodyCanonizationResult = bodyCanonizationSimple();
103 break;
104 case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
105 mBodyCanonizationResult = bodyCanonizationRelaxed();
106 break;
107 }
108 // qDebug() << " bodyCanonizationResult "<< mBodyCanonizationResult << " algorithm " << mDkimInfo.hashingAlgorithm() << mDkimInfo.bodyHash();
109
110 if (mDkimInfo.bodyLengthCount() != -1) { // Verify it.
111 if (mDkimInfo.bodyLengthCount() > mBodyCanonizationResult.length()) {
112 // length tag exceeds body size
113 qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << " mDkimInfo.bodyLengthCount() " << mDkimInfo.bodyLengthCount() << " mBodyCanonizationResult.length() "
114 << mBodyCanonizationResult.length();
115 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::SignatureTooLarge;
116 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
117 Q_EMIT result(createCheckResult());
118 deleteLater();
119 return;
120 } else if (mDkimInfo.bodyLengthCount() < mBodyCanonizationResult.length()) {
121 mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::SignatureTooSmall;
122 }
123 // truncated body to the length specified in the "l=" tag
124 mBodyCanonizationResult = mBodyCanonizationResult.left(mDkimInfo.bodyLengthCount());
125 }
126 if (mBodyCanonizationResult.startsWith(QLatin1StringView("\r\n"))) { // Remove it from start
127 mBodyCanonizationResult = mBodyCanonizationResult.right(mBodyCanonizationResult.length() - 2);
128 }
129 // It seems that kmail add a space before this line => it breaks check
130 if (mBodyCanonizationResult.startsWith(QLatin1StringView(" This is a multi-part message in MIME format"))) { // Remove it from start
131 mBodyCanonizationResult.replace(QStringLiteral(" This is a multi-part message in MIME format"),
132 QStringLiteral("This is a multi-part message in MIME format"));
133 }
134 // It seems that kmail add a space before this line => it breaks check
135 if (mBodyCanonizationResult.startsWith(QLatin1StringView(" This is a cryptographically signed message in MIME format."))) { // Remove it from start
136 mBodyCanonizationResult.replace(QStringLiteral(" This is a cryptographically signed message in MIME format."),
137 QStringLiteral("This is a cryptographically signed message in MIME format."));
138 }
139 if (mBodyCanonizationResult.startsWith(QLatin1StringView(" \r\n"))) { // Remove it from start
140 static const QRegularExpression reg{QStringLiteral("^ \r\n")};
141 mBodyCanonizationResult.remove(reg);
142 }
143#ifdef DEBUG_SIGNATURE_DKIM
144 QFile caFile(QStringLiteral("/tmp/bodycanon-kmail.txt"));
147 outStream << mBodyCanonizationResult;
148 caFile.close();
149#endif
150
152 switch (mDkimInfo.hashingAlgorithm()) {
153 case DKIMInfo::HashingAlgorithmType::Sha1:
154 resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha1);
155 break;
156 case DKIMInfo::HashingAlgorithmType::Sha256:
157 resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha256);
158 break;
159 case DKIMInfo::HashingAlgorithmType::Any:
160 case DKIMInfo::HashingAlgorithmType::Unknown:
161 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InsupportedHashAlgorithm;
162 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
163 Q_EMIT result(createCheckResult());
164 deleteLater();
165 return;
166 }
167
168 // compare body hash
169 qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "resultHash " << resultHash << "mDkimInfo.bodyHash()" << mDkimInfo.bodyHash();
170 if (resultHash != mDkimInfo.bodyHash().toLatin1()) {
171 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Corrupted body hash";
172 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::CorruptedBodyHash;
173 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
174 Q_EMIT result(createCheckResult());
175 deleteLater();
176 return;
177 }
178
179 if (mDkimInfo.headerCanonization() == MessageViewer::DKIMInfo::CanonicalizationType::Unknown) {
180 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidHeaderCanonicalization;
181 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
182 Q_EMIT result(createCheckResult());
183 deleteLater();
184 return;
185 }
186 // Parse message header
187 if (!mHeaderParser.wasAlreadyParsed()) {
188 mHeaderParser.setHead(mMessage->head());
189 mHeaderParser.parse();
190 }
191
192 computeHeaderCanonization(true);
193 if (mPolicy.saveKey() == MessageViewer::MessageViewerSettings::EnumSaveKey::Save) {
194 const QString keyValue = MessageViewer::DKIMManagerKey::self()->keyValue(mDkimInfo.selector(), mDkimInfo.domain());
195 // qDebug() << " mDkimInfo.selector() " << mDkimInfo.selector() << "mDkimInfo.domain() " << mDkimInfo.domain() << keyValue;
196 if (keyValue.isEmpty()) {
197 downloadKey(mDkimInfo);
198 } else {
199 parseDKIMKeyRecord(keyValue, mDkimInfo.domain(), mDkimInfo.selector(), false);
200 MessageViewer::DKIMManagerKey::self()->updateLastUsed(mDkimInfo.domain(), mDkimInfo.selector());
201 }
202 } else {
203 downloadKey(mDkimInfo);
204 }
205}
206
207void DKIMCheckSignatureJob::computeHeaderCanonization(bool removeQuoteOnContentType)
208{
209 // Compute Hash Header
210 switch (mDkimInfo.headerCanonization()) {
211 case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
212 return;
213 case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
214 mHeaderCanonizationResult = headerCanonizationSimple();
215 break;
216 case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
217 mHeaderCanonizationResult = headerCanonizationRelaxed(removeQuoteOnContentType);
218 break;
219 }
220
221 // In hash step 2, the Signer/Verifier MUST pass the following to the
222 // hash algorithm in the indicated order.
223
224 // 1. The header fields specified by the "h=" tag, in the order
225 // specified in that tag, and canonicalized using the header
226 // canonicalization algorithm specified in the "c=" tag. Each
227 // header field MUST be terminated with a single CRLF.
228
229 // 2. The DKIM-Signature header field that exists (verifying) or will
230 // be inserted (signing) in the message, with the value of the "b="
231 // tag (including all surrounding whitespace) deleted (i.e., treated
232 // as the empty string), canonicalized using the header
233 // canonicalization algorithm specified in the "c=" tag, and without
234 // a trailing CRLF.
235 // add DKIM-Signature header to the hash input
236 // with the value of the "b=" tag (including all surrounding whitespace) deleted
237
238 // Add dkim-signature as lowercase
239
240 QString dkimValue = mDkimValue;
241 dkimValue = dkimValue.left(dkimValue.indexOf(QLatin1StringView("b=")) + 2);
242 switch (mDkimInfo.headerCanonization()) {
243 case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
244 return;
245 case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
246 mHeaderCanonizationResult += QLatin1StringView("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QStringLiteral("dkim-signature"), dkimValue);
247 break;
248 case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
249 mHeaderCanonizationResult += QLatin1StringView("\r\n")
250 + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QStringLiteral("dkim-signature"), dkimValue, removeQuoteOnContentType);
251 break;
252 }
253#ifdef DEBUG_SIGNATURE_DKIM
255 QStringLiteral("/tmp/headercanon-kmail-%1.txt").arg(removeQuoteOnContentType ? QLatin1StringView("removequote") : QLatin1StringView("withquote")));
258 outHeaderStream << mHeaderCanonizationResult;
259 headerFile.close();
260#endif
261}
262
263void DKIMCheckSignatureJob::setHeaderParser(const DKIMHeaderParser &headerParser)
264{
265 mHeaderParser = headerParser;
266}
267
268void DKIMCheckSignatureJob::setCheckSignatureAuthenticationResult(const QList<DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult> &lst)
269{
270 mCheckSignatureAuthenticationResult = lst;
271}
272
273QString DKIMCheckSignatureJob::bodyCanonizationSimple() const
274{
275 /*
276 * canonicalize the body using the simple algorithm
277 * specified in Section 3.4.3 of RFC 6376
278 */
279 // The "simple" body canonicalization algorithm ignores all empty lines
280 // at the end of the message body. An empty line is a line of zero
281 // length after removal of the line terminator. If there is no body or
282 // no trailing CRLF on the message body, a CRLF is added. It makes no
283 // other changes to the message body. In more formal terms, the
284 // "simple" body canonicalization algorithm converts "*CRLF" at the end
285 // of the body to a single "CRLF".
286
287 // Note that a completely empty or missing body is canonicalized as a
288 // single "CRLF"; that is, the canonicalized length will be 2 octets.
289
290 return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->encodedBody()));
291}
292
293QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const
294{
295 /*
296 * canonicalize the body using the relaxed algorithm
297 * specified in Section 3.4.4 of RFC 6376
298 */
299 /*
300 a. Reduce whitespace:
301
302 * Ignore all whitespace at the end of lines. Implementations
303 MUST NOT remove the CRLF at the end of the line.
304
305 * Reduce all sequences of WSP within a line to a single SP
306 character.
307
308 b. Ignore all empty lines at the end of the message body. "Empty
309 line" is defined in Section 3.4.3. If the body is non-empty but
310 does not end with a CRLF, a CRLF is added. (For email, this is
311 only possible when using extensions to SMTP or non-SMTP transport
312 mechanisms.)
313 */
314 const QString returnValue = MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromLatin1(mMessage->encodedBody()));
315 return returnValue;
316}
317
318QString DKIMCheckSignatureJob::headerCanonizationSimple() const
319{
320 QString headers;
321
322 DKIMHeaderParser parser = mHeaderParser;
323
324 const auto listSignedHeader{mDkimInfo.listSignedHeader()};
325 for (const QString &header : listSignedHeader) {
326 const QString str = parser.headerType(header.toLower());
327 if (!str.isEmpty()) {
328 if (!headers.isEmpty()) {
329 headers += QLatin1StringView("\r\n");
330 }
331 headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, str);
332 }
333 }
334 return headers;
335}
336
337QString DKIMCheckSignatureJob::headerCanonizationRelaxed(bool removeQuoteOnContentType) const
338{
339 // The "relaxed" header canonicalization algorithm MUST apply the
340 // following steps in order:
341
342 // o Convert all header field names (not the header field values) to
343 // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
344
345 // o Unfold all header field continuation lines as described in
346 // [RFC5322]; in particular, lines with terminators embedded in
347 // continued header field values (that is, CRLF sequences followed by
348 // WSP) MUST be interpreted without the CRLF. Implementations MUST
349 // NOT remove the CRLF at the end of the header field value.
350
351 // o Convert all sequences of one or more WSP characters to a single SP
352 // character. WSP characters here include those before and after a
353 // line folding boundary.
354
355 // o Delete all WSP characters at the end of each unfolded header field
356 // value.
357
358 // o Delete any WSP characters remaining before and after the colon
359 // separating the header field name from the header field value. The
360 // colon separator MUST be retained.
361
362 QString headers;
363 DKIMHeaderParser parser = mHeaderParser;
364 const auto listSignedHeader = mDkimInfo.listSignedHeader();
365 for (const QString &header : listSignedHeader) {
366 const QString str = parser.headerType(header.toLower());
367 if (!str.isEmpty()) {
368 if (!headers.isEmpty()) {
369 headers += QLatin1StringView("\r\n");
370 }
371 headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, str, removeQuoteOnContentType);
372 }
373 }
374 return headers;
375}
376
377void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info)
378{
379 auto job = new DKIMDownloadKeyJob(this);
380 job->setDomainName(info.domain());
381 job->setSelectorName(info.selector());
382 connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) {
383 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString;
384 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
385 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
386 Q_EMIT result(createCheckResult());
387 deleteLater();
388 });
389 connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone);
390
391 if (!job->start()) {
392 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey";
393 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
394 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
395 Q_EMIT result(createCheckResult());
396 deleteLater();
397 }
398}
399
400void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList<QByteArray> &lst, const QString &domain, const QString &selector)
401{
403 if (lst.count() != 1) {
404 for (const QByteArray &b : lst) {
405 ba += b;
406 }
407 // qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst;
408 } else {
409 ba = lst.at(0);
410 }
411 parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true);
412}
413
414void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue)
415{
417 << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) key:" << str;
418 if (!mDkimKeyRecord.parseKey(str)) {
419 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str;
420 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
421 Q_EMIT result(createCheckResult());
422 deleteLater();
423 return;
424 }
425 const QString keyType{mDkimKeyRecord.keyType()};
426 if ((keyType != QLatin1StringView("rsa")) && (keyType != QLatin1StringView("ed25519"))) {
427 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << keyType << " str " << str;
428 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
429 Q_EMIT result(createCheckResult());
430 deleteLater();
431 return;
432 }
433
434 // if s flag is set in DKIM key record
435 // AUID must be from the same domain as SDID (and not a subdomain)
436 if (mDkimKeyRecord.flags().contains(QLatin1StringView("s"))) {
437 // s Any DKIM-Signature header fields using the "i=" tag MUST have
438 // the same domain value on the right-hand side of the "@" in the
439 // "i=" tag and the value of the "d=" tag. That is, the "i="
440 // domain MUST NOT be a subdomain of "d=". Use of this flag is
441 // RECOMMENDED unless subdomaining is required.
442 if (mDkimInfo.iDomain() != mDkimInfo.domain()) {
443 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
444 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI;
445 Q_EMIT result(createCheckResult());
446 deleteLater();
447 return;
448 }
449 }
450 // TODO add support for ed25119
451
452 // check that the testing flag is not set
453 if (mDkimKeyRecord.flags().contains(QLatin1StringView("y"))) {
454 if (!mPolicy.verifySignatureWhenOnlyTest()) {
455 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!";
456 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode;
457 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
458 Q_EMIT result(createCheckResult());
459 deleteLater();
460 return;
461 }
462 }
463 if (mDkimKeyRecord.publicKey().isEmpty()) {
464 // empty value means that this public key has been revoked
465 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked ";
466 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked;
467 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
468 Q_EMIT result(createCheckResult());
469 deleteLater();
470 return;
471 }
472
473 if (storeKeyValue) {
474 Q_EMIT storeKey(str, domain, selector);
475 }
476
477 verifySignature();
478}
479
480void DKIMCheckSignatureJob::verifySignature()
481{
482 const QString keyType{mDkimKeyRecord.keyType()};
483 if (keyType == QLatin1StringView("rsa")) {
484 verifyRSASignature();
485 } else if (keyType == QLatin1StringView("ed25519")) {
486 verifyEd25519Signature();
487 } else {
488 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " It's a bug " << keyType;
489 }
490}
491
492void DKIMCheckSignatureJob::verifyEd25519Signature()
493{
494 // TODO implement it.
495 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "it's a Ed25519 signed email";
496 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
497 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
498 Q_EMIT result(createCheckResult());
499 deleteLater();
500}
501
502void DKIMCheckSignatureJob::verifyRSASignature()
503{
505 // qDebug() << "mDkimKeyRecord.publicKey() " <<mDkimKeyRecord.publicKey() << " QCA::base64ToArray(mDkimKeyRecord.publicKey() "
506 // <<QCA::base64ToArray(mDkimKeyRecord.publicKey());
507 QCA::PublicKey publicKey = QCA::RSAPublicKey::fromDER(QCA::base64ToArray(mDkimKeyRecord.publicKey()), &conversionResult);
509 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Public key read failed" << conversionResult << " public key" << mDkimKeyRecord.publicKey();
510 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
511 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
512 Q_EMIT result(createCheckResult());
513 deleteLater();
514 return;
515 } else {
516 qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Success loading public key";
517 }
519 // qDebug() << "publicKey.modulus" << rsaPublicKey.n().toString();
520 // qDebug() << "publicKey.exponent" << rsaPublicKey.e().toString();
521
522 if (rsaPublicKey.e().toString().toLong() * 4 < 1024) {
523 const int publicRsaTooSmallPolicyValue = mPolicy.publicRsaTooSmallPolicy();
524 if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Nothing) {
525 // Nothing
526 } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Warning) {
527 mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::PublicRsaKeyTooSmall;
528 } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Error) {
529 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyTooSmall;
530 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
531 Q_EMIT result(createCheckResult());
532 deleteLater();
533 return;
534 }
535
536 } else if (rsaPublicKey.e().toString().toLong() * 4 < 2048) {
537 // TODO
538 }
539 // qDebug() << "mHeaderCanonizationResult " << mHeaderCanonizationResult << " mDkimInfo.signature() " << mDkimInfo.signature();
540 if (rsaPublicKey.canVerify()) {
541 const QString s = mDkimInfo.signature().remove(QLatin1Char(' '));
542 QCA::SecureArray sec = mHeaderCanonizationResult.toLatin1();
544 // qDebug() << " s base ba" << ba;
546 switch (mDkimInfo.hashingAlgorithm()) {
547 case DKIMInfo::HashingAlgorithmType::Sha1:
549 break;
550 case DKIMInfo::HashingAlgorithmType::Sha256:
552 break;
553 case DKIMInfo::HashingAlgorithmType::Any:
554 case DKIMInfo::HashingAlgorithmType::Unknown: {
555 // then signature is invalid
556 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
557 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
558 Q_EMIT result(createCheckResult());
559 deleteLater();
560 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "DKIMInfo::HashingAlgorithmType undefined ! ";
561 return;
562 }
563 }
564 if (!rsaPublicKey.verifyMessage(sec, ba, sigAlg, QCA::DERSequence)) {
565 computeHeaderCanonization(false);
566 const QCA::SecureArray secWithoutQuote = mHeaderCanonizationResult.toLatin1();
567 if (!rsaPublicKey.verifyMessage(secWithoutQuote, ba, sigAlg, QCA::DERSequence)) {
568 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature invalid";
569 // then signature is invalid
570 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
571 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
572 Q_EMIT result(createCheckResult());
573 deleteLater();
574 return;
575 }
576 }
577 } else {
578 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to verify signature";
579 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
580 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
581 Q_EMIT result(createCheckResult());
582 deleteLater();
583 return;
584 }
585 mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
586 Q_EMIT result(createCheckResult());
587 deleteLater();
588}
589
590DKIMCheckPolicy DKIMCheckSignatureJob::policy() const
591{
592 return mPolicy;
593}
594
595void DKIMCheckSignatureJob::setPolicy(const DKIMCheckPolicy &policy)
596{
597 mPolicy = policy;
598}
599
600DKIMCheckSignatureJob::DKIMWarning DKIMCheckSignatureJob::warning() const
601{
602 return mWarning;
603}
604
605void DKIMCheckSignatureJob::setWarning(DKIMCheckSignatureJob::DKIMWarning warning)
606{
607 mWarning = warning;
608}
609
610KMime::Message::Ptr DKIMCheckSignatureJob::message() const
611{
612 return mMessage;
613}
614
615void DKIMCheckSignatureJob::setMessage(const KMime::Message::Ptr &message)
616{
617 mMessage = message;
618}
619
620MessageViewer::DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::checkSignature(const DKIMInfo &info)
621{
622 const qint64 currentDate = QDateTime::currentSecsSinceEpoch();
623 if (info.expireTime() != -1 && info.expireTime() < currentDate) {
624 mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureExpired;
625 }
626 if (info.signatureTimeStamp() != -1 && info.signatureTimeStamp() > currentDate) {
627 mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture;
628 }
629 if (info.signature().isEmpty()) {
630 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist";
631 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature;
632 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
633 }
634 if (!info.listSignedHeader().contains(QLatin1StringView("from"), Qt::CaseInsensitive)) {
635 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list";
636 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom;
637 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
638 }
639 if (info.domain().isEmpty()) {
640 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined.";
641 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist;
642 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
643 }
644 if (info.query() != QLatin1StringView("dns/txt")) {
645 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query();
646 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod;
647 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
648 }
649
650 if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any)
651 || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) {
652 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty";
653 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm;
654 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
655 }
656 if (info.signingAlgorithm().isEmpty()) {
657 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty";
658 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm;
659 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
660 }
661
662 if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) {
663 if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) {
664 // nothing
665 } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) {
666 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error";
667 mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe;
668 } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) {
669 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error";
670 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1;
671 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
672 }
673 }
674
675 // qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain();
676 if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) {
677 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID";
678 mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError;
679 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
680 }
681 // Add more test
682 // TODO check if info is valid
683 return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
684}
685
686DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const
687{
688 return mError;
689}
690
691DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const
692{
693 return mStatus;
694}
695
696void DKIMCheckSignatureJob::setStatus(DKIMCheckSignatureJob::DKIMStatus status)
697{
698 mStatus = status;
699}
700
701QString DKIMCheckSignatureJob::dkimValue() const
702{
703 return mDkimValue;
704}
705
706bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const
707{
708 return status != DKIMCheckSignatureJob::DKIMStatus::Unknown;
709}
710
711bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
712{
713 return error == other.error && warning == other.warning && status == other.status && fromEmail == other.fromEmail && auid == other.auid
714 && sdid == other.sdid && listSignatureAuthenticationResult == other.listSignatureAuthenticationResult;
715}
716
717bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
718{
719 return !CheckSignatureResult::operator==(other);
720}
721
722QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::CheckSignatureResult &t)
723{
724 d << " error " << t.error;
725 d << " warning " << t.warning;
726 d << " status " << t.status;
727 d << " signedBy " << t.sdid;
728 d << " fromEmail " << t.fromEmail;
729 d << " auid " << t.auid;
730 d << " authenticationResult " << t.listSignatureAuthenticationResult;
731 return d;
732}
733
734QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &t)
735{
736 d << " method " << t.method;
737 d << " errorStr " << t.errorStr;
738 d << " status " << t.status;
739 d << " sdid " << t.sdid;
740 d << " auid " << t.auid;
741 d << " inforesult " << t.infoResult;
742 return d;
743}
744
745bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::operator==(const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &other) const
746{
747 return errorStr == other.errorStr && method == other.method && status == other.status && sdid == other.sdid && auid == other.auid
748 && infoResult == other.infoResult;
749}
750
751bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::isValid() const
752{
753 // TODO improve it
754 return (method != AuthenticationMethod::Unknown);
755}
756
757#include "moc_dkimchecksignaturejob.cpp"
The DKIMCheckPolicy class.
The DKIMDownloadKeyJob class.
The DKIMHeaderParser class.
The DKIMInfo class.
Definition dkiminfo.h:20
RSAPublicKey toRSA() const
static PublicKey fromDER(const QByteArray &a, ConvertResult *result=nullptr, const QString &provider=QString())
Q_SCRIPTABLE CaptureState status()
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
QDebug operator<<(QDebug dbg, const PerceptualColor::LchaDouble &value)
ConvertResult
ConvertGood
SignatureAlgorithm
EMSA3_SHA1
EMSA3_SHA256
DERSequence
QCA_EXPORT QByteArray base64ToArray(const QString &base64String)
qint64 currentSecsSinceEpoch()
const_reference at(qsizetype i) const const
qsizetype count() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
T qobject_cast(QObject *object)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
QString fromLocal8Bit(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString right(qsizetype n) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.