Messagelib

dkimchecksignaturejob.cpp
1 /*
2  SPDX-FileCopyrightText: 2018-2022 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  if (mDkimKeyRecord.keyType() != QLatin1String("rsa")) {
425  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << mDkimKeyRecord.keyType() << " str " << str;
426  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
427  Q_EMIT result(createCheckResult());
428  deleteLater();
429  return;
430  }
431 
432  // if s flag is set in DKIM key record
433  // AUID must be from the same domain as SDID (and not a subdomain)
434  if (mDkimKeyRecord.flags().contains(QLatin1String("s"))) {
435  // s Any DKIM-Signature header fields using the "i=" tag MUST have
436  // the same domain value on the right-hand side of the "@" in the
437  // "i=" tag and the value of the "d=" tag. That is, the "i="
438  // domain MUST NOT be a subdomain of "d=". Use of this flag is
439  // RECOMMENDED unless subdomaining is required.
440  if (mDkimInfo.iDomain() != mDkimInfo.domain()) {
441  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
442  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI;
443  Q_EMIT result(createCheckResult());
444  deleteLater();
445  return;
446  }
447  }
448  // check that the testing flag is not set
449  if (mDkimKeyRecord.flags().contains(QLatin1String("y"))) {
450  if (!mPolicy.verifySignatureWhenOnlyTest()) {
451  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!";
452  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode;
453  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
454  Q_EMIT result(createCheckResult());
455  deleteLater();
456  return;
457  }
458  }
459  if (mDkimKeyRecord.publicKey().isEmpty()) {
460  // empty value means that this public key has been revoked
461  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked ";
462  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked;
463  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
464  Q_EMIT result(createCheckResult());
465  deleteLater();
466  return;
467  }
468 
469  if (storeKeyValue) {
470  Q_EMIT storeKey(str, domain, selector);
471  }
472 
473  verifyRSASignature();
474 }
475 
476 void DKIMCheckSignatureJob::verifyRSASignature()
477 {
478  QCA::ConvertResult conversionResult;
479  // qDebug() << "mDkimKeyRecord.publicKey() " <<mDkimKeyRecord.publicKey() << " QCA::base64ToArray(mDkimKeyRecord.publicKey() "
480  // <<QCA::base64ToArray(mDkimKeyRecord.publicKey());
481  QCA::PublicKey publicKey = QCA::RSAPublicKey::fromDER(QCA::base64ToArray(mDkimKeyRecord.publicKey()), &conversionResult);
482  if (QCA::ConvertGood != conversionResult) {
483  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Public key read failed" << conversionResult << " public key" << mDkimKeyRecord.publicKey();
484  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyConversionError;
485  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
486  Q_EMIT result(createCheckResult());
487  deleteLater();
488  return;
489  } else {
490  qDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Success loading public key";
491  }
492  QCA::RSAPublicKey rsaPublicKey = publicKey.toRSA();
493  // qDebug() << "publicKey.modulus" << rsaPublicKey.n().toString();
494  // qDebug() << "publicKey.exponent" << rsaPublicKey.e().toString();
495 
496  if (rsaPublicKey.e().toString().toLong() * 4 < 1024) {
497  const int publicRsaTooSmallPolicyValue = mPolicy.publicRsaTooSmallPolicy();
498  if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Nothing) {
499  // Nothing
500  } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Warning) {
501  mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::PublicRsaKeyTooSmall;
502  } else if (publicRsaTooSmallPolicyValue == MessageViewer::MessageViewerSettings::EnumPublicRsaTooSmall::Error) {
503  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyTooSmall;
504  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
505  Q_EMIT result(createCheckResult());
506  deleteLater();
507  return;
508  }
509 
510  } else if (rsaPublicKey.e().toString().toLong() * 4 < 2048) {
511  // TODO
512  }
513  // qDebug() << "mHeaderCanonizationResult " << mHeaderCanonizationResult << " mDkimInfo.signature() " << mDkimInfo.signature();
514  if (rsaPublicKey.canVerify()) {
515  const QString s = mDkimInfo.signature().remove(QLatin1Char(' '));
516  QCA::SecureArray sec = mHeaderCanonizationResult.toLatin1();
517  const QByteArray ba = QCA::base64ToArray(s);
518  // qDebug() << " s base ba" << ba;
520  switch (mDkimInfo.hashingAlgorithm()) {
521  case DKIMInfo::HashingAlgorithmType::Sha1:
522  sigAlg = QCA::EMSA3_SHA1;
523  break;
524  case DKIMInfo::HashingAlgorithmType::Sha256:
525  sigAlg = QCA::EMSA3_SHA256;
526  break;
527  case DKIMInfo::HashingAlgorithmType::Any:
528  case DKIMInfo::HashingAlgorithmType::Unknown: {
529  // then signature is invalid
530  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
531  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
532  Q_EMIT result(createCheckResult());
533  deleteLater();
534  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "DKIMInfo::HashingAlgorithmType undefined ! ";
535  return;
536  }
537  }
538  if (!rsaPublicKey.verifyMessage(sec, ba, sigAlg, QCA::DERSequence)) {
539  computeHeaderCanonization(false);
540  const QCA::SecureArray secWithoutQuote = mHeaderCanonizationResult.toLatin1();
541  if (!rsaPublicKey.verifyMessage(secWithoutQuote, ba, sigAlg, QCA::DERSequence)) {
542  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature invalid";
543  // then signature is invalid
544  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
545  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
546  Q_EMIT result(createCheckResult());
547  deleteLater();
548  return;
549  }
550  }
551  } else {
552  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to verify signature";
553  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToVerifySignature;
554  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
555  Q_EMIT result(createCheckResult());
556  deleteLater();
557  return;
558  }
559  mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
560  Q_EMIT result(createCheckResult());
561  deleteLater();
562 }
563 
564 DKIMCheckPolicy DKIMCheckSignatureJob::policy() const
565 {
566  return mPolicy;
567 }
568 
569 void DKIMCheckSignatureJob::setPolicy(const DKIMCheckPolicy &policy)
570 {
571  mPolicy = policy;
572 }
573 
574 DKIMCheckSignatureJob::DKIMWarning DKIMCheckSignatureJob::warning() const
575 {
576  return mWarning;
577 }
578 
579 void DKIMCheckSignatureJob::setWarning(DKIMCheckSignatureJob::DKIMWarning warning)
580 {
581  mWarning = warning;
582 }
583 
584 KMime::Message::Ptr DKIMCheckSignatureJob::message() const
585 {
586  return mMessage;
587 }
588 
589 void DKIMCheckSignatureJob::setMessage(const KMime::Message::Ptr &message)
590 {
591  mMessage = message;
592 }
593 
594 MessageViewer::DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::checkSignature(const DKIMInfo &info)
595 {
596  const qint64 currentDate = QDateTime::currentSecsSinceEpoch();
597  if (info.expireTime() != -1 && info.expireTime() < currentDate) {
598  mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureExpired;
599  }
600  if (info.signatureTimeStamp() != -1 && info.signatureTimeStamp() > currentDate) {
601  mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture;
602  }
603  if (info.signature().isEmpty()) {
604  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist";
605  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature;
606  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
607  }
608  if (!info.listSignedHeader().contains(QLatin1String("from"), Qt::CaseInsensitive)) {
609  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list";
610  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom;
611  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
612  }
613  if (info.domain().isEmpty()) {
614  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined.";
615  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist;
616  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
617  }
618  if (info.query() != QLatin1String("dns/txt")) {
619  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query();
620  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod;
621  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
622  }
623 
624  if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any)
625  || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) {
626  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty";
627  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm;
628  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
629  }
630  if (info.signingAlgorithm().isEmpty()) {
631  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty";
632  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm;
633  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
634  }
635 
636  if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) {
637  if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) {
638  // nothing
639  } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) {
640  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error";
641  mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe;
642  } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) {
643  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error";
644  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1;
645  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
646  }
647  }
648 
649  // qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain();
650  if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) {
651  qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID";
652  mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError;
653  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid;
654  }
655  // Add more test
656  // TODO check if info is valid
657  return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid;
658 }
659 
660 DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const
661 {
662  return mError;
663 }
664 
665 DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const
666 {
667  return mStatus;
668 }
669 
670 void DKIMCheckSignatureJob::setStatus(DKIMCheckSignatureJob::DKIMStatus status)
671 {
672  mStatus = status;
673 }
674 
675 QString DKIMCheckSignatureJob::dkimValue() const
676 {
677  return mDkimValue;
678 }
679 
680 bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const
681 {
682  return status != DKIMCheckSignatureJob::DKIMStatus::Unknown;
683 }
684 
685 bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
686 {
687  return error == other.error && warning == other.warning && status == other.status && fromEmail == other.fromEmail && auid == other.auid
688  && sdid == other.sdid && listSignatureAuthenticationResult == other.listSignatureAuthenticationResult;
689 }
690 
691 bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const
692 {
693  return !CheckSignatureResult::operator==(other);
694 }
695 
696 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::CheckSignatureResult &t)
697 {
698  d << " error " << t.error;
699  d << " warning " << t.warning;
700  d << " status " << t.status;
701  d << " signedBy " << t.sdid;
702  d << " fromEmail " << t.fromEmail;
703  d << " auid " << t.auid;
704  d << " authenticationResult " << t.listSignatureAuthenticationResult;
705  return d;
706 }
707 
708 QDebug operator<<(QDebug d, const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &t)
709 {
710  d << " method " << t.method;
711  d << " errorStr " << t.errorStr;
712  d << " status " << t.status;
713  d << " sdid " << t.sdid;
714  d << " auid " << t.auid;
715  d << " inforesult " << t.infoResult;
716  return d;
717 }
718 
719 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::operator==(const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &other) const
720 {
721  return errorStr == other.errorStr && method == other.method && status == other.status && sdid == other.sdid && auid == other.auid
722  && infoResult == other.infoResult;
723 }
724 
725 bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::isValid() const
726 {
727  // TODO improve it
728  return (method != AuthenticationMethod::Unknown);
729 }
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
KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::Alarm::Ptr &)
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())
QByteArray toLatin1() const const
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
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
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
QCA_EXPORT QByteArray base64ToArray(const QString &base64String)
int length() const const
const T & at(int i) const const
bool canVerify() const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
The DKIMHeaderParser class.
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-2022 The KDE developers.
Generated on Tue May 24 2022 04:08:03 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.