1 /* t-tofuinfo.cpp 2 3 This file is part of qgpgme, the Qt API binding for gpgme 4 Copyright (c) 2016 by Bundesamt für Sicherheit in der Informationstechnik 5 Software engineering by Intevation GmbH 6 7 QGpgME is free software; you can redistribute it and/or 8 modify it under the terms of the GNU General Public License as 9 published by the Free Software Foundation; either version 2 of the 10 License, or (at your option) any later version. 11 12 QGpgME is distributed in the hope that it will be useful, 13 but WITHOUT ANY WARRANTY; without even the implied warranty of 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 General Public License for more details. 16 17 You should have received a copy of the GNU General Public License 18 along with this program; if not, write to the Free Software 19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 21 In addition, as a special exception, the copyright holders give 22 permission to link the code of this program with any edition of 23 the Qt library by Trolltech AS, Norway (or with modified versions 24 of Qt that use the same license as Qt), and distribute linked 25 combinations including the two. You must obey the GNU General 26 Public License in all respects for all of the code used other than 27 Qt. If you modify this file, you may extend this exception to 28 your version of the file, but you are not obligated to do so. If 29 you do not wish to do so, delete this exception statement from 30 your version. 31 */ 32 #ifdef HAVE_CONFIG_H 33 #include "config.h" 34 #endif 35 36 #include <QDebug> 37 #include <QTest> 38 #include <QTemporaryDir> 39 #include <QSignalSpy> 40 41 #include "protocol.h" 42 #include "tofuinfo.h" 43 #include "tofupolicyjob.h" 44 #include "verifyopaquejob.h" 45 #include "verificationresult.h" 46 #include "signingresult.h" 47 #include "importjob.h" 48 #include "importresult.h" 49 #include "keylistjob.h" 50 #include "keylistresult.h" 51 #include "signjob.h" 52 #include "key.h" 53 #include "t-support.h" 54 #include "engineinfo.h" 55 #include "context.h" 56 #include <iostream> 57 58 using namespace QGpgME; 59 using namespace GpgME; 60 61 static const char testMsg1[] = 62 "-----BEGIN PGP MESSAGE-----\n" 63 "\n" 64 "owGbwMvMwCSoW1RzPCOz3IRxjXQSR0lqcYleSUWJTZOvjVdpcYmCu1+oQmaJIleH\n" 65 "GwuDIBMDGysTSIqBi1MApi+nlGGuwDeHao53HBr+FoVGP3xX+kvuu9fCMJvl6IOf\n" 66 "y1kvP4y+8D5a11ang0udywsA\n" 67 "=Crq6\n" 68 "-----END PGP MESSAGE-----\n"; 69 70 static const char conflictKey1[] = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" 71 "\n" 72 "mDMEXDWgpxYJKwYBBAHaRw8BAQdAguVu4qkx8iw4eU+TQ4vvcKG7IdcZvbMhw3Zc\n" 73 "npGf0+u0GXRvZnVfY29uZmxpY3RAZXhhbXBsZS5jb22IkAQTFggAOBYhBO6ovNDG\n" 74 "nLzbR1TlMJYJ0fjlWbUrBQJcNaCnAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA\n" 75 "AAoJEJYJ0fjlWbUrLaMBALegwkv2+sEcmKZqxt8JscYvFiEuycv2+rKHaZA0eDoN\n" 76 "AP97W4XrJb5x49J5jDDdeko8k00uGqiiuAXJo27/i/phA7g4BFw1oKcSCisGAQQB\n" 77 "l1UBBQEBB0Crhw24E2lPBhd/y+ZFotQ/2TrYqkUQqGPmff8ofLziNgMBCAeIeAQY\n" 78 "FggAIBYhBO6ovNDGnLzbR1TlMJYJ0fjlWbUrBQJcNaCnAhsMAAoJEJYJ0fjlWbUr\n" 79 "/K8BAJWsa+tOZsJw7w5fz6O0We6Xx4Rt17jHf563G6wMcz9+AQDRsedJ7w4zYzS9\n" 80 "MFiJQ5aN0NDHMRtDFWAgCunVnJ3OBw==\n" 81 "=fZa5\n" 82 "-----END PGP PUBLIC KEY BLOCK-----\n"; 83 84 static const char conflictKey2[] = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" 85 "\n" 86 "mDMEXDWgixYJKwYBBAHaRw8BAQdAMWOhumYspcvEOTuesOSN4rvnJVOj/6qOWFTu\n" 87 "x+wPRra0GXRvZnVfY29uZmxpY3RAZXhhbXBsZS5jb22IkAQTFggAOBYhBA64G88Q\n" 88 "NPXztj8ID/FhC7tiGbeRBQJcNaCLAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA\n" 89 "AAoJEPFhC7tiGbeRUt4A/2hf4Zgz+TYyfeH/4/ZtyL1JuZggqR1s5UopEx2Aiw10\n" 90 "AP405KiTd31TJQN8Ru+7bskPu0/mzLZMNkRvBNEdc5kbDLg4BFw1oIsSCisGAQQB\n" 91 "l1UBBQEBB0B5NtSrx7wDDKgwUe5Rxz0vRkaWLtyE0KbfE77oPy5DGAMBCAeIeAQY\n" 92 "FggAIBYhBA64G88QNPXztj8ID/FhC7tiGbeRBQJcNaCLAhsMAAoJEPFhC7tiGbeR\n" 93 "km0BAP8TQwraipqb1pJlLsEgDXeM5Jocz4fuePD78BsOBtORAP9gpCyKXdyJYGlA\n" 94 "qjmG356yG6pCK9aPckTZ9IViPiHWCw==\n" 95 "=tn3Q\n" 96 "-----END PGP PUBLIC KEY BLOCK-----\n"; 97 98 static const char conflictMsg1[] = "-----BEGIN PGP MESSAGE-----\n" 99 "\n" 100 "owGbwMvMwCE2jfPij6eRW7UZTwsnMcSYLnT0Ki0uUXD3C1XILFHk6ihlYRDjYJAV\n" 101 "U2R5t2LPhWNz9tx2D3lqANPEygTSwcDFKQAT+RjG8M9of873hQrMpinBVwKYv+rq\n" 102 "XGmYW+ZcZJ+133KDq+itzlxGhg3L2X/6Khj+2Hd+He+KnXtunF2wNWxl7849e/Sy\n" 103 "v6tc+8MBAA==\n" 104 "=fZLe\n" 105 "-----END PGP MESSAGE-----\n"; 106 107 static const char conflictMsg2[] = "-----BEGIN PGP MESSAGE-----\n" 108 "\n" 109 "owGbwMvMwCH2MZF7d5Lk9omMp4WTGGJMFwZ4lRaXKLj7hSpklihydZSyMIhxMMiK\n" 110 "KbLw7ZA+L2Dy9fM2ew5+mCZWJpAOBi5OAZhIUhIjw7bV+xS+cR0quqhmcY2Dl3WW\n" 111 "8Ufr+rRNufOPyIdoO6nEXGH47/B+E1+oxS6e5f5n7MJ3aHBO+s345sipGV/4f665\n" 112 "9mmiGjsA\n" 113 "=8oJA\n" 114 "-----END PGP MESSAGE-----\n"; 115 116 class TofuInfoTest: public QGpgMETest 117 { 118 Q_OBJECT 119 Q_SIGNALS: 120 void asyncDone(); 121 122 private: testSupported()123 bool testSupported() 124 { 125 static bool initialized, supported; 126 if (initialized) { 127 return supported; 128 } 129 initialized = true; 130 if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.16") { 131 return false; 132 } 133 // If the keylist fails here this means that gnupg does not 134 // support tofu at all. It can be disabled at compile time. So no 135 // tests. 136 auto *job = openpgp()->keyListJob(false, false, false); 137 job->addMode(GpgME::WithTofu); 138 std::vector<GpgME::Key> keys; 139 job->exec(QStringList() << QStringLiteral("zulu@example.net"), true, keys); 140 delete job; 141 supported = !keys.empty(); 142 return supported; 143 } 144 testTofuCopy(TofuInfo other,const TofuInfo & orig)145 void testTofuCopy(TofuInfo other, const TofuInfo &orig) 146 { 147 QVERIFY(!orig.isNull()); 148 QVERIFY(!other.isNull()); 149 QVERIFY(orig.signLast() == other.signLast()); 150 QVERIFY(orig.signCount() == other.signCount()); 151 QVERIFY(orig.validity() == other.validity()); 152 QVERIFY(orig.policy() == other.policy()); 153 } 154 signAndVerify(const QString & what,const GpgME::Key & key,int expected)155 void signAndVerify(const QString &what, const GpgME::Key &key, int expected) 156 { 157 auto job = openpgp()->signJob(); 158 auto ctx = Job::context(job); 159 TestPassphraseProvider provider; 160 ctx->setPassphraseProvider(&provider); 161 ctx->setPinentryMode(Context::PinentryLoopback); 162 163 std::vector<Key> keys; 164 keys.push_back(key); 165 QByteArray signedData; 166 auto sigResult = job->exec(keys, what.toUtf8(), NormalSignatureMode, signedData); 167 delete job; 168 169 QVERIFY(!sigResult.error()); 170 foreach (const auto uid, keys[0].userIDs()) { 171 auto info = uid.tofuInfo(); 172 QVERIFY(info.signCount() == expected - 1); 173 } 174 175 auto verifyJob = openpgp()->verifyOpaqueJob(); 176 QByteArray verified; 177 178 auto result = verifyJob->exec(signedData, verified); 179 delete verifyJob; 180 181 QVERIFY(!result.error()); 182 QVERIFY(verified == what.toUtf8()); 183 184 QVERIFY(result.numSignatures() == 1); 185 auto sig = result.signatures()[0]; 186 187 auto key2 = sig.key(); 188 QVERIFY(!key.isNull()); 189 QVERIFY(!strcmp (key2.primaryFingerprint(), key.primaryFingerprint())); 190 QVERIFY(!strcmp (key.primaryFingerprint(), sig.fingerprint())); 191 auto stats = key2.userID(0).tofuInfo(); 192 QVERIFY(!stats.isNull()); 193 if (stats.signCount() != expected) { 194 std::cout << "################ Key before verify: " 195 << key 196 << "################ Key after verify: " 197 << key2; 198 } 199 QVERIFY(stats.signCount() == expected); 200 } 201 202 private Q_SLOTS: testTofuNull()203 void testTofuNull() 204 { 205 if (!testSupported()) { 206 return; 207 } 208 TofuInfo tofu; 209 QVERIFY(tofu.isNull()); 210 QVERIFY(!tofu.description()); 211 QVERIFY(!tofu.signCount()); 212 QVERIFY(!tofu.signLast()); 213 QVERIFY(!tofu.signFirst()); 214 QVERIFY(tofu.validity() == TofuInfo::ValidityUnknown); 215 QVERIFY(tofu.policy() == TofuInfo::PolicyUnknown); 216 } 217 testTofuInfo()218 void testTofuInfo() 219 { 220 if (!testSupported()) { 221 return; 222 } 223 auto *job = openpgp()->verifyOpaqueJob(true); 224 const QByteArray data1(testMsg1); 225 QByteArray plaintext; 226 227 auto ctx = Job::context(job); 228 QVERIFY(ctx); 229 ctx->setSender("alfa@example.net"); 230 231 auto result = job->exec(data1, plaintext); 232 delete job; 233 234 QVERIFY(!result.isNull()); 235 QVERIFY(!result.error()); 236 QVERIFY(!strcmp(plaintext.constData(), "Just GNU it!\n")); 237 238 QVERIFY(result.numSignatures() == 1); 239 Signature sig = result.signatures()[0]; 240 /* TOFU is always marginal */ 241 QVERIFY(sig.validity() == Signature::Marginal); 242 243 auto stats = sig.key().userID(0).tofuInfo(); 244 QVERIFY(!stats.isNull()); 245 QVERIFY(sig.key().primaryFingerprint()); 246 QVERIFY(sig.fingerprint()); 247 QVERIFY(!strcmp(sig.key().primaryFingerprint(), sig.fingerprint())); 248 QVERIFY(stats.signFirst() == stats.signLast()); 249 QVERIFY(stats.signCount() == 1); 250 QVERIFY(stats.policy() == TofuInfo::PolicyAuto); 251 QVERIFY(stats.validity() == TofuInfo::LittleHistory); 252 253 testTofuCopy(stats, stats); 254 255 /* Another verify */ 256 257 job = openpgp()->verifyOpaqueJob(true); 258 result = job->exec(data1, plaintext); 259 delete job; 260 261 QVERIFY(!result.isNull()); 262 QVERIFY(!result.error()); 263 264 QVERIFY(result.numSignatures() == 1); 265 sig = result.signatures()[0]; 266 /* TOFU is always marginal */ 267 QVERIFY(sig.validity() == Signature::Marginal); 268 269 stats = sig.key().userID(0).tofuInfo(); 270 QVERIFY(!stats.isNull()); 271 QVERIFY(!strcmp(sig.key().primaryFingerprint(), sig.fingerprint())); 272 QVERIFY(stats.signFirst() == stats.signLast()); 273 QVERIFY(stats.signCount() == 1); 274 QVERIFY(stats.policy() == TofuInfo::PolicyAuto); 275 QVERIFY(stats.validity() == TofuInfo::LittleHistory); 276 277 /* Verify that another call yields the same result */ 278 job = openpgp()->verifyOpaqueJob(true); 279 result = job->exec(data1, plaintext); 280 delete job; 281 282 QVERIFY(!result.isNull()); 283 QVERIFY(!result.error()); 284 285 QVERIFY(result.numSignatures() == 1); 286 sig = result.signatures()[0]; 287 /* TOFU is always marginal */ 288 QVERIFY(sig.validity() == Signature::Marginal); 289 290 stats = sig.key().userID(0).tofuInfo(); 291 QVERIFY(!stats.isNull()); 292 QVERIFY(!strcmp(sig.key().primaryFingerprint(), sig.fingerprint())); 293 QVERIFY(stats.signFirst() == stats.signLast()); 294 QVERIFY(stats.signCount() == 1); 295 QVERIFY(stats.policy() == TofuInfo::PolicyAuto); 296 QVERIFY(stats.validity() == TofuInfo::LittleHistory); 297 } 298 testTofuSignCount()299 void testTofuSignCount() 300 { 301 if (!testSupported()) { 302 return; 303 } 304 auto *job = openpgp()->keyListJob(false, false, false); 305 job->addMode(GpgME::WithTofu); 306 std::vector<GpgME::Key> keys; 307 GpgME::KeyListResult result = job->exec(QStringList() << QStringLiteral("zulu@example.net"), 308 true, keys); 309 delete job; 310 QVERIFY(!keys.empty()); 311 Key key = keys[0]; 312 QVERIFY(!key.isNull()); 313 314 /* As we sign & verify quickly here we need different 315 * messages to avoid having them treated as the same 316 * message if they were created within the same second. 317 * Alternatively we could use the same message and wait 318 * a second between each call. But this would slow down 319 * the testsuite. */ 320 signAndVerify(QStringLiteral("Hello"), key, 1); 321 key.update(); 322 signAndVerify(QStringLiteral("Hello2"), key, 2); 323 key.update(); 324 signAndVerify(QStringLiteral("Hello3"), key, 3); 325 key.update(); 326 signAndVerify(QStringLiteral("Hello4"), key, 4); 327 } 328 testTofuKeyList()329 void testTofuKeyList() 330 { 331 if (!testSupported()) { 332 return; 333 } 334 335 /* First check that the key has no tofu info. */ 336 auto *job = openpgp()->keyListJob(false, false, false); 337 std::vector<GpgME::Key> keys; 338 auto result = job->exec(QStringList() << QStringLiteral("zulu@example.net"), 339 true, keys); 340 delete job; 341 QVERIFY(!keys.empty()); 342 auto key = keys[0]; 343 QVERIFY(!key.isNull()); 344 QVERIFY(key.userID(0).tofuInfo().isNull()); 345 auto keyCopy = key; 346 keyCopy.update(); 347 auto sigCnt = keyCopy.userID(0).tofuInfo().signCount(); 348 signAndVerify(QStringLiteral("Hello5"), keyCopy, 349 sigCnt + 1); 350 keyCopy.update(); 351 signAndVerify(QStringLiteral("Hello6"), keyCopy, 352 sigCnt + 2); 353 354 /* Now another one but with tofu */ 355 job = openpgp()->keyListJob(false, false, false); 356 job->addMode(GpgME::WithTofu); 357 result = job->exec(QStringList() << QStringLiteral("zulu@example.net"), 358 true, keys); 359 delete job; 360 QVERIFY(!result.error()); 361 QVERIFY(!keys.empty()); 362 auto key2 = keys[0]; 363 QVERIFY(!key2.isNull()); 364 auto info = key2.userID(0).tofuInfo(); 365 QVERIFY(!info.isNull()); 366 QVERIFY(info.signCount()); 367 } 368 testTofuPolicy()369 void testTofuPolicy() 370 { 371 if (!testSupported()) { 372 return; 373 } 374 375 /* First check that the key has no tofu info. */ 376 auto *job = openpgp()->keyListJob(false, false, false); 377 std::vector<GpgME::Key> keys; 378 job->addMode(GpgME::WithTofu); 379 auto result = job->exec(QStringList() << QStringLiteral("bravo@example.net"), 380 false, keys); 381 382 if (keys.empty()) { 383 qDebug() << "bravo@example.net not found"; 384 qDebug() << "Error: " << result.error().asString(); 385 const auto homedir = QString::fromLocal8Bit(qgetenv("GNUPGHOME")); 386 qDebug() << "Homedir is: " << homedir; 387 QFileInfo fi(homedir + "/pubring.gpg"); 388 qDebug () << "pubring exists: " << fi.exists() << " readable? " 389 << fi.isReadable() << " size: " << fi.size(); 390 QFileInfo fi2(homedir + "/pubring.kbx"); 391 qDebug () << "keybox exists: " << fi2.exists() << " readable? " 392 << fi2.isReadable() << " size: " << fi2.size(); 393 394 result = job->exec(QStringList(), false, keys); 395 foreach (const auto key, keys) { 396 qDebug() << "Key: " << key.userID(0).name() << " <" 397 << key.userID(0).email() 398 << ">\n fpr: " << key.primaryFingerprint(); 399 } 400 } 401 QVERIFY(!result.error()); 402 QVERIFY(!keys.empty()); 403 auto key = keys[0]; 404 QVERIFY(!key.isNull()); 405 QVERIFY(key.userID(0).tofuInfo().policy() != TofuInfo::PolicyBad); 406 auto *tofuJob = openpgp()->tofuPolicyJob(); 407 auto err = tofuJob->exec(key, TofuInfo::PolicyBad); 408 QVERIFY(!err); 409 result = job->exec(QStringList() << QStringLiteral("bravo@example.net"), 410 false, keys); 411 QVERIFY(!keys.empty()); 412 key = keys[0]; 413 QVERIFY(key.userID(0).tofuInfo().policy() == TofuInfo::PolicyBad); 414 err = tofuJob->exec(key, TofuInfo::PolicyGood); 415 416 result = job->exec(QStringList() << QStringLiteral("bravo@example.net"), 417 false, keys); 418 key = keys[0]; 419 QVERIFY(key.userID(0).tofuInfo().policy() == TofuInfo::PolicyGood); 420 delete tofuJob; 421 delete job; 422 } 423 testTofuConflict()424 void testTofuConflict() 425 { 426 if (!testSupported()) { 427 return; 428 } 429 430 if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.19") { 431 return; 432 } 433 434 // Import key 1 435 auto importjob = openpgp()->importJob(); 436 connect(importjob, &ImportJob::result, this, 437 [this](ImportResult result, QString, Error) 438 { 439 QVERIFY(!result.error()); 440 QVERIFY(!result.imports().empty()); 441 QVERIFY(result.numImported()); 442 Q_EMIT asyncDone(); 443 }); 444 importjob->start(QByteArray(conflictKey1)); 445 QSignalSpy spy (this, SIGNAL(asyncDone())); 446 QVERIFY(spy.wait()); 447 448 // Verify Message 1 449 const QByteArray signedData(conflictMsg1); 450 auto verifyJob = openpgp()->verifyOpaqueJob(true); 451 QByteArray verified; 452 auto result = verifyJob->exec(signedData, verified); 453 delete verifyJob; 454 455 QVERIFY(!result.isNull()); 456 QVERIFY(!result.error()); 457 458 QVERIFY(result.numSignatures() == 1); 459 auto sig = result.signatures()[0]; 460 QVERIFY(sig.validity() == Signature::Marginal); 461 462 auto stats = sig.key().userID(0).tofuInfo(); 463 QVERIFY(!stats.isNull()); 464 QVERIFY(!strcmp(sig.key().primaryFingerprint(), sig.fingerprint())); 465 QVERIFY(stats.signFirst() == stats.signLast()); 466 QVERIFY(stats.signCount() == 1); 467 QVERIFY(stats.policy() == TofuInfo::PolicyAuto); 468 QVERIFY(stats.validity() == TofuInfo::LittleHistory); 469 470 // Import key 2 471 importjob = openpgp()->importJob(); 472 connect(importjob, &ImportJob::result, this, 473 [this](ImportResult result, QString, Error) 474 { 475 QVERIFY(!result.error()); 476 QVERIFY(!result.imports().empty()); 477 QVERIFY(result.numImported()); 478 Q_EMIT asyncDone(); 479 }); 480 importjob->start(QByteArray(conflictKey2)); 481 QSignalSpy spy2 (this, SIGNAL(asyncDone())); 482 QVERIFY(spy2.wait()); 483 484 // Verify Message 2 485 const QByteArray signedData2(conflictMsg2); 486 QByteArray verified2; 487 verifyJob = openpgp()->verifyOpaqueJob(true); 488 result = verifyJob->exec(signedData2, verified2); 489 delete verifyJob; 490 491 QVERIFY(!result.isNull()); 492 QVERIFY(!result.error()); 493 494 QVERIFY(result.numSignatures() == 1); 495 sig = result.signatures()[0]; 496 QVERIFY(sig.validity() == Signature::Unknown); 497 // TODO activate when implemented 498 // QVERIFY(sig.summary() == Signature::TofuConflict); 499 500 stats = sig.key().userID(0).tofuInfo(); 501 QVERIFY(!stats.isNull()); 502 QVERIFY(!strcmp(sig.key().primaryFingerprint(), sig.fingerprint())); 503 QVERIFY(stats.signFirst() == stats.signLast()); 504 QVERIFY(stats.signCount() == 1); 505 QVERIFY(stats.policy() == TofuInfo::PolicyAsk); 506 QVERIFY(stats.validity() == TofuInfo::Conflict); 507 } 508 509 initTestCase()510 void initTestCase() 511 { 512 QGpgMETest::initTestCase(); 513 const QString gpgHome = qgetenv("GNUPGHOME"); 514 qputenv("GNUPGHOME", mDir.path().toUtf8()); 515 QVERIFY(mDir.isValid()); 516 QFile conf(mDir.path() + QStringLiteral("/gpg.conf")); 517 QVERIFY(conf.open(QIODevice::WriteOnly)); 518 conf.write("trust-model tofu+pgp"); 519 conf.close(); 520 QFile agentConf(mDir.path() + QStringLiteral("/gpg-agent.conf")); 521 QVERIFY(agentConf.open(QIODevice::WriteOnly)); 522 agentConf.write("allow-loopback-pinentry"); 523 agentConf.close(); 524 QVERIFY(copyKeyrings(gpgHome, mDir.path())); 525 } 526 private: 527 QTemporaryDir mDir; 528 529 }; 530 531 QTEST_MAIN(TofuInfoTest) 532 533 #include "t-tofuinfo.moc" 534