Messagelib

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

KDE's Doxygen guidelines are available online.