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

KDE's Doxygen guidelines are available online.