Messagelib

dkimchecksignaturejob.cpp
1 /*
2  SPDX-FileCopyrightText: 2018-2023 Laurent Montel <[email protected]>
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
24 using namespace MessageViewer;
25 DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent)
26  : QObject(parent)
27 {
28 }
29 
30 DKIMCheckSignatureJob::~DKIMCheckSignatureJob() = default;
31 
32 MessageViewer::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 
45 QString DKIMCheckSignatureJob::bodyCanonizationResult() const
46 {
47  return mBodyCanonizationResult;
48 }
49 
50 QString DKIMCheckSignatureJob::headerCanonizationResult() const
51 {
52  return mHeaderCanonizationResult;
53 }
54 
55 void 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());
61  deleteLater();
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());
74  deleteLater();
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());
82  deleteLater();
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());
90  deleteLater();
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());
99  deleteLater();
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(QLatin1String("\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(QLatin1String(" 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(QLatin1String(" 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(QLatin1String(" \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"));
145  caFile.open(QIODevice::WriteOnly | QIODevice::Text);
146  QTextStream outStream(&caFile);
147  outStream << mBodyCanonizationResult;
148  caFile.close();
149 #endif
150 
151  QByteArray resultHash;
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 
207 void 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(QLatin1String("b=")) + 2);
242  switch (mDkimInfo.headerCanonization()) {
243  case MessageViewer::DKIMInfo::CanonicalizationType::Unknown:
244  return;
245  case MessageViewer::DKIMInfo::CanonicalizationType::Simple:
246  mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QStringLiteral("dkim-signature"), dkimValue);
247  break;
248  case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed:
249  mHeaderCanonizationResult +=
250  QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QStringLiteral("dkim-signature"), dkimValue, removeQuoteOnContentType);
251  break;
252  }
253 #ifdef DEBUG_SIGNATURE_DKIM
254  QFile headerFile(QStringLiteral("/tmp/headercanon-kmail-%1.txt").arg(removeQuoteOnContentType ? QLatin1String("removequote") : QLatin1String("withquote")));
255  headerFile.open(QIODevice::WriteOnly | QIODevice::Text);
256  QTextStream outHeaderStream(&headerFile);
257  outHeaderStream << mHeaderCanonizationResult;
258  headerFile.close();
259 #endif
260 }
261 
262 void DKIMCheckSignatureJob::setHeaderParser(const DKIMHeaderParser &headerParser)
263 {
264  mHeaderParser = headerParser;
265 }
266 
267 void DKIMCheckSignatureJob::setCheckSignatureAuthenticationResult(const QVector<DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult> &lst)
268 {
269  mCheckSignatureAuthenticationResult = lst;
270 }
271 
272 QString DKIMCheckSignatureJob::bodyCanonizationSimple() const
273 {
274  /*
275  * canonicalize the body using the simple algorithm
276  * specified in Section 3.4.3 of RFC 6376
277  */
278  // The "simple" body canonicalization algorithm ignores all empty lines
279  // at the end of the message body. An empty line is a line of zero
280  // length after removal of the line terminator. If there is no body or
281  // no trailing CRLF on the message body, a CRLF is added. It makes no
282  // other changes to the message body. In more formal terms, the
283  // "simple" body canonicalization algorithm converts "*CRLF" at the end
284  // of the body to a single "CRLF".
285 
286  // Note that a completely empty or missing body is canonicalized as a
287  // single "CRLF"; that is, the canonicalized length will be 2 octets.
288 
289  return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->encodedBody()));
290 }
291 
292 QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const
293 {
294  /*
295  * canonicalize the body using the relaxed algorithm
296  * specified in Section 3.4.4 of RFC 6376
297  */
298  /*
299  a. Reduce whitespace:
300 
301  * Ignore all whitespace at the end of lines. Implementations
302  MUST NOT remove the CRLF at the end of the line.
303 
304  * Reduce all sequences of WSP within a line to a single SP
305  character.
306 
307  b. Ignore all empty lines at the end of the message body. "Empty
308  line" is defined in Section 3.4.3. If the body is non-empty but
309  does not end with a CRLF, a CRLF is added. (For email, this is
310  only possible when using extensions to SMTP or non-SMTP transport
311  mechanisms.)
312  */
313  const QString returnValue = MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromLatin1(mMessage->encodedBody()));
314  return returnValue;
315 }
316 
317 QString DKIMCheckSignatureJob::headerCanonizationSimple() const
318 {
319  QString headers;
320 
321  DKIMHeaderParser parser = mHeaderParser;
322 
323  const auto listSignedHeader{mDkimInfo.listSignedHeader()};
324  for (const QString &header : listSignedHeader) {
325  const QString str = parser.headerType(header.toLower());
326  if (!str.isEmpty()) {
327  if (!headers.isEmpty()) {
328  headers += QLatin1String("\r\n");
329  }
330  headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, str);
331  }
332  }
333  return headers;
334 }
335 
336 QString DKIMCheckSignatureJob::headerCanonizationRelaxed(bool removeQuoteOnContentType) const
337 {
338  // The "relaxed" header canonicalization algorithm MUST apply the
339  // following steps in order:
340 
341  // o Convert all header field names (not the header field values) to
342  // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC".
343 
344  // o Unfold all header field continuation lines as described in
345  // [RFC5322]; in particular, lines with terminators embedded in
346  // continued header field values (that is, CRLF sequences followed by
347  // WSP) MUST be interpreted without the CRLF. Implementations MUST
348  // NOT remove the CRLF at the end of the header field value.
349 
350  // o Convert all sequences of one or more WSP characters to a single SP
351  // character. WSP characters here include those before and after a
352  // line folding boundary.
353 
354  // o Delete all WSP characters at the end of each unfolded header field
355  // value.
356 
357  // o Delete any WSP characters remaining before and after the colon
358  // separating the header field name from the header field value. The
359  // colon separator MUST be retained.
360 
361  QString headers;
362  DKIMHeaderParser parser = mHeaderParser;
363  const auto listSignedHeader = mDkimInfo.listSignedHeader();
364  for (const QString &header : listSignedHeader) {
365  const QString str = parser.headerType(header.toLower());
366  if (!str.isEmpty()) {
367  if (!headers.isEmpty()) {
368  headers += QLatin1String("\r\n");
369  }
370  headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, str, removeQuoteOnContentType);
371  }
372  }
373  return headers;
374 }
375 
376 void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info)
377 {
378  auto job = new DKIMDownloadKeyJob(this);
379  job->setDomainName(info.domain());
380  job->setSelectorName(info.selector());
381  connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) {
382  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString;
383  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
384  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
385  Q_EMIT result(createCheckResult());
386  deleteLater();
387  });
388  connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone);
389 
390  if (!job->start()) {
391  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey";
392  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey;
393  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
394  Q_EMIT result(createCheckResult());
395  deleteLater();
396  }
397 }
398 
399 void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList<QByteArray> &lst, const QString &domain, const QString &selector)
400 {
401  QByteArray ba;
402  if (lst.count() != 1) {
403  for (const QByteArray &b : lst) {
404  ba += b;
405  }
406  // qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst;
407  } else {
408  ba = lst.at(0);
409  }
410  parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true);
411 }
412 
413 void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue)
414 {
415  qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG)
416  << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) key:" << str;
417  if (!mDkimKeyRecord.parseKey(str)) {
418  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str;
419  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
420  Q_EMIT result(createCheckResult());
421  deleteLater();
422  return;
423  }
424  const QString keyType{mDkimKeyRecord.keyType()};
425  if ((keyType != QLatin1String("rsa")) && (keyType != QLatin1String("ed25519"))) {
426  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << keyType << " str " << str;
427  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
428  Q_EMIT result(createCheckResult());
429  deleteLater();
430  return;
431  }
432 
433  // if s flag is set in DKIM key record
434  // AUID must be from the same domain as SDID (and not a subdomain)
435  if (mDkimKeyRecord.flags().contains(QLatin1String("s"))) {
436  // s Any DKIM-Signature header fields using the "i=" tag MUST have
437  // the same domain value on the right-hand side of the "@" in the
438  // "i=" tag and the value of the "d=" tag. That is, the "i="
439  // domain MUST NOT be a subdomain of "d=". Use of this flag is
440  // RECOMMENDED unless subdomaining is required.
441  if (mDkimInfo.iDomain() != mDkimInfo.domain()) {
442  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
443  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI;
444  Q_EMIT result(createCheckResult());
445  deleteLater();
446  return;
447  }
448  }
449  // TODO add support for ed25119
450 
451  // check that the testing flag is not set
452  if (mDkimKeyRecord.flags().contains(QLatin1String("y"))) {
453  if (!mPolicy.verifySignatureWhenOnlyTest()) {
454  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!";
455  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode;
456  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
457  Q_EMIT result(createCheckResult());
458  deleteLater();
459  return;
460  }
461  }
462  if (mDkimKeyRecord.publicKey().isEmpty()) {
463  // empty value means that this public key has been revoked
464  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked ";
465  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked;
466  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
467  Q_EMIT result(createCheckResult());
468  deleteLater();
469  return;
470  }
471 
472  if (storeKeyValue) {
473  Q_EMIT storeKey(str, domain, selector);
474  }
475 
476  verifySignature();
477 }
478 
479 void DKIMCheckSignatureJob::verifySignature()
480 {
481  const QString keyType{mDkimKeyRecord.keyType()};
482  if (keyType == QLatin1String("rsa")) {
483  verifyRSASignature();
484  } else if (keyType == QLatin1String("ed25519")) {
485  verifyEd25519Signature();
486  } else {
487  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " It's a bug " << keyType;
488  }
489 }
490 
491 void DKIMCheckSignatureJob::verifyEd25519Signature()
492 {
493  // TODO implement it.
494  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "it's a Ed25519 signed email";
495  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
496  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
497  Q_EMIT result(createCheckResult());
498  deleteLater();
499 }
500 
501 void DKIMCheckSignatureJob::verifyRSASignature()
502 {
503  QCA::ConvertResult conversionResult;
504  // qDebug() << "mDkimKeyRecord.publicKey() " <<mDkimKeyRecord.publicKey() << " QCA::base64ToArray(mDkimKeyRecord.publicKey() "
505  // <<QCA::base64ToArray(mDkimKeyRecord.publicKey());
506  QCA::PublicKey publicKey = QCA::RSAPublicKey::fromDER(QCA::base64ToArray(mDkimKeyRecord.publicKey()), &conversionResult);
507  if (QCA::ConvertGood != conversionResult) {
508  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Public key read failed" << conversionResult << " public key" << mDkimKeyRecord.publicKey();
509  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
510  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
511  Q_EMIT result(createCheckResult());
512  deleteLater();
513  return;
514  } else {
515  qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Success loading public key";
516  }
517  QCA::RSAPublicKey rsaPublicKey = publicKey.toRSA();
518  // qDebug() << "publicKey.modulus" << rsaPublicKey.n().toString();
519  // qDebug() << "publicKey.exponent" << rsaPublicKey.e().toString();
520 
521  if (rsaPublicKey.e().toString().toLong() * 4 < 1024) {
522  const int publicRsaTooSmallPolicyValue = mPolicy.publicRsaTooSmallPolicy();
523  if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Nothing) {
524  // Nothing
525  } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Warning) {
526  mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::PublicRsaKeyTooSmall;
527  } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Error) {
528  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyTooSmall;
529  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
530  Q_EMIT result(createCheckResult());
531  deleteLater();
532  return;
533  }
534 
535  } else if (rsaPublicKey.e().toString().toLong() * 4 < 2048) {
536  // TODO
537  }
538  // qDebug() << "mHeaderCanonizationResult " << mHeaderCanonizationResult << " mDkimInfo.signature() " << mDkimInfo.signature();
539  if (rsaPublicKey.canVerify()) {
540  const QString s = mDkimInfo.signature().remove(QLatin1Char(' '));
541  QCA::SecureArray sec = mHeaderCanonizationResult.toLatin1();
542  const QByteArray ba = QCA::base64ToArray(s);
543  // qDebug() << " s base ba" << ba;
545  switch (mDkimInfo.hashingAlgorithm()) {
546  case DKIMInfo::HashingAlgorithmType::Sha1:
547  sigAlg = QCA::EMSA3_SHA1;
548  break;
549  case DKIMInfo::HashingAlgorithmType::Sha256:
550  sigAlg = QCA::EMSA3_SHA256;
551  break;
552  case DKIMInfo::HashingAlgorithmType::Any:
553  case DKIMInfo::HashingAlgorithmType::Unknown: {
554  // then signature is invalid
555  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
556  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
557  Q_EMIT result(createCheckResult());
558  deleteLater();
559  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "DKIMInfo::HashingAlgorithmType undefined ! ";
560  return;
561  }
562  }
563  if (!rsaPublicKey.verifyMessage(sec, ba, sigAlg, QCA::DERSequence)) {
564  computeHeaderCanonization(false);
565  const QCA::SecureArray secWithoutQuote = mHeaderCanonizationResult.toLatin1();
566  if (!rsaPublicKey.verifyMessage(secWithoutQuote, ba, sigAlg, QCA::DERSequence)) {
567  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature invalid";
568  // then signature is invalid
569  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
570  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
571  Q_EMIT result(createCheckResult());
572  deleteLater();
573  return;
574  }
575  }
576  } else {
577  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to verify signature";
578  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
579  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
580  Q_EMIT result(createCheckResult());
581  deleteLater();
582  return;
583  }
584  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
585  Q_EMIT result(createCheckResult());
586  deleteLater();
587 }
588 
589 DKIMCheckPolicy DKIMCheckSignatureJob::policy() const
590 {
591  return mPolicy;
592 }
593 
594 void DKIMCheckSignatureJob::setPolicy(const DKIMCheckPolicy &policy)
595 {
596  mPolicy = policy;
597 }
598 
599 DKIMCheckSignatureJob::DKIMWarning DKIMCheckSignatureJob::warning() const
600 {
601  return mWarning;
602 }
603 
604 void DKIMCheckSignatureJob::setWarning(DKIMCheckSignatureJob::DKIMWarning warning)
605 {
606  mWarning = warning;
607 }
608 
609 KMime::Message::Ptr DKIMCheckSignatureJob::message() const
610 {
611  return mMessage;
612 }
613 
614 void DKIMCheckSignatureJob::setMessage(const KMime::Message::Ptr &message)
615 {
616  mMessage = message;
617 }
618 
619 MessageViewer::DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::checkSignature(const DKIMInfo &info)
620 {
621  const qint64 currentDate = QDateTime::currentSecsSinceEpoch();
622  if (info.expireTime() != -1 && info.expireTime() < currentDate) {
623  mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureExpired;
624  }
625  if (info.signatureTimeStamp() != -1 && info.signatureTimeStamp() > currentDate) {
626  mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture;
627  }
628  if (info.signature().isEmpty()) {
629  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist";
630  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature;
631  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
632  }
633  if (!info.listSignedHeader().contains(QLatin1String("from"), Qt::CaseInsensitive)) {
634  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list";
635  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom;
636  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
637  }
638  if (info.domain().isEmpty()) {
639  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined.";
640  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist;
641  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
642  }
643  if (info.query() != QLatin1String("dns/txt")) {
644  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query();
645  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod;
646  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
647  }
648 
649  if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any)
650  || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) {
651  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty";
652  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm;
653  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
654  }
655  if (info.signingAlgorithm().isEmpty()) {
656  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty";
657  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm;
658  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
659  }
660 
661  if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) {
662  if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) {
663  // nothing
664  } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) {
665  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error";
666  mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe;
667  } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) {
668  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error";
669  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1;
670  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
671  }
672  }
673 
674  // qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain();
675  if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) {
676  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID";
677  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError;
678  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
679  }
680  // Add more test
681  // TODO check if info is valid
682  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
683 }
684 
685 DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const
686 {
687  return mError;
688 }
689 
690 DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const
691 {
692  return mStatus;
693 }
694 
695 void DKIMCheckSignatureJob::setStatus(DKIMCheckSignatureJob::DKIMStatus status)
696 {
697  mStatus = status;
698 }
699 
700 QString DKIMCheckSignatureJob::dkimValue() const
701 {
702  return mDkimValue;
703 }
704 
705 bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const
706 {
707  return status != DKIMCheckSignatureJob::DKIMStatus::Unknown;
708 }
709 
710 bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
711 {
712  return error == other.error && warning == other.warning && status == other.status && fromEmail == other.fromEmail && auid == other.auid
713  && sdid == other.sdid && listSignatureAuthenticationResult == other.listSignatureAuthenticationResult;
714 }
715 
716 bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
717 {
718  return !CheckSignatureResult::operator==(other);
719 }
720 
721 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::CheckSignatureResult &t)
722 {
723  d << " error " << t.error;
724  d << " warning " << t.warning;
725  d << " status " << t.status;
726  d << " signedBy " << t.sdid;
727  d << " fromEmail " << t.fromEmail;
728  d << " auid " << t.auid;
729  d << " authenticationResult " << t.listSignatureAuthenticationResult;
730  return d;
731 }
732 
733 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &t)
734 {
735  d << " method " << t.method;
736  d << " errorStr " << t.errorStr;
737  d << " status " << t.status;
738  d << " sdid " << t.sdid;
739  d << " auid " << t.auid;
740  d << " inforesult " << t.infoResult;
741  return d;
742 }
743 
744 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::operator==(const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &other) const
745 {
746  return errorStr == other.errorStr && method == other.method && status == other.status && sdid == other.sdid && auid == other.auid
747  && infoResult == other.infoResult;
748 }
749 
750 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::isValid() const
751 {
752  // TODO improve it
753  return (method != AuthenticationMethod::Unknown);
754 }
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
bool verifyMessage(const MemoryRegion &a, const QByteArray &sig, SignatureAlgorithm alg, SignatureFormat format=DefaultFormat)
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
CaseInsensitive
Q_EMITQ_EMIT
QString toString() const
int count(const T &value) const const
The DKIMCheckPolicy class.
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
static PublicKey fromDER(const QByteArray &a, ConvertResult *result=nullptr, const QString &provider=QString())
QDataStream & operator<<(QDataStream &out, const KDateTime &dateTime)
QByteArray toLatin1() const const
qint64 currentSecsSinceEpoch()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
SignatureAlgorithm
void deleteLater()
QString fromLocal8Bit(const char *str, int size)
bool isEmpty() const const
QCA_EXPORT QByteArray base64ToArray(const QString &base64String)
int length() const const
const T & at(int i) const const
Q_SCRIPTABLE CaptureState status()
bool canVerify() const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
The DKIMHeaderParser class.
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString & replace(int position, int n, QChar after)
QString & remove(int position, int n)
RSAPublicKey toRSA() const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
EMSA3_SHA256
ConvertResult
QString left(int n) const const
QString right(int n) const const
QString fromLatin1(const char *str, int size)
The DKIMDownloadKeyJob class.
The DKIMInfo class.
Definition: dkiminfo.h:19
EMSA3_SHA1
QString message
BigInteger e() const
long toLong(bool *ok, int base) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Fri Mar 24 2023 04:08:31 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.