qt: Extend SignKeyJob to create signatures with expiration date
* lang/qt/src/signkeyjob.h (SignKeyJob::setExpirationDate): New.
* lang/qt/src/qgpgmesignkeyjob.h, lang/qt/src/qgpgmesignkeyjob.cpp
(QGpgMESignKeyJob::setExpirationDate): New.
* lang/qt/src/qgpgmesignkeyjob.cpp (QGpgMESignKeyJob::Private): Add
member m_expiration.
(sign_key): Handle expiration date.
(QGpgMESignKeyJob::start): Pass expiration date to sign_key.
* lang/qt/tests/t-various.cpp
(TestVarious::testSignKeyWithoutExpiration,
TestVarious::testSignKeyWithExpiration): New.
(TestVarious::initTestCase): Add "allow-weak-key-signatures" to
gpg.conf.
--
This allows Kleopatra (and other users of QGpgme) to create key
signatures with expiration date.
GnuPG-bug-id: 5336, 5506
Ingo Klöcker
2 years ago
37 | 37 | |
38 | 38 | #include "qgpgmesignkeyjob.h" |
39 | 39 | |
40 | #include <QDate> | |
40 | 41 | #include <QString> |
41 | 42 | |
42 | 43 | #include "dataprovider.h" |
44 | 45 | #include "context.h" |
45 | 46 | #include "data.h" |
46 | 47 | #include "gpgsignkeyeditinteractor.h" |
48 | ||
49 | #include "qgpgme_debug.h" | |
47 | 50 | |
48 | 51 | #include <cassert> |
49 | 52 | |
73 | 76 | bool m_dupeOk = false; |
74 | 77 | QString m_remark; |
75 | 78 | TrustSignatureProperties m_trustSignature; |
79 | QDate m_expiration; | |
76 | 80 | }; |
77 | 81 | |
78 | 82 | QGpgMESignKeyJob::QGpgMESignKeyJob(Context *context) |
87 | 91 | static QGpgMESignKeyJob::result_type sign_key(Context *ctx, const Key &key, const std::vector<unsigned int> &uids, |
88 | 92 | unsigned int checkLevel, const Key &signer, unsigned int opts, |
89 | 93 | bool dupeOk, const QString &remark, |
90 | const TrustSignatureProperties &trustSignature) | |
94 | const TrustSignatureProperties &trustSignature, | |
95 | const QDate &expirationDate) | |
91 | 96 | { |
92 | 97 | QGpgME::QByteArrayDataProvider dp; |
93 | 98 | Data data(&dp); |
113 | 118 | skei->setTrustSignatureScope(trustSignature.scope.toUtf8().toStdString()); |
114 | 119 | } |
115 | 120 | |
116 | if (!signer.isNull()) | |
121 | if (!signer.isNull()) { | |
117 | 122 | if (const Error err = ctx->addSigningKey(signer)) { |
118 | 123 | return std::make_tuple(err, QString(), Error()); |
119 | 124 | } |
125 | } | |
126 | ||
127 | if (expirationDate.isValid()) { | |
128 | // on 2106-02-07, the Unix time will reach 0xFFFFFFFF; since gpg uses uint32 internally | |
129 | // for the expiration date clip it at 2106-02-06 | |
130 | static const QDate maxAllowedDate{2106, 2, 6}; | |
131 | const auto clippedExpirationDate = expirationDate <= maxAllowedDate ? expirationDate : maxAllowedDate; | |
132 | if (clippedExpirationDate != expirationDate) { | |
133 | qCWarning(QGPGME_LOG) << "Expiration of certification has been changed to" << clippedExpirationDate; | |
134 | } | |
135 | // use the "days from now" format to specify the expiration date of the certification; | |
136 | // this format is the most appropriate regardless of the local timezone | |
137 | const auto daysFromNow = QDate::currentDate().daysTo(clippedExpirationDate); | |
138 | if (daysFromNow > 0) { | |
139 | const auto certExpire = std::to_string(daysFromNow) + "d"; | |
140 | ctx->setFlag("cert-expire", certExpire.c_str()); | |
141 | } | |
142 | } else { | |
143 | // explicitly set "cert-expire" to "0" (no expiration) to override default-cert-expire set in gpg.conf | |
144 | ctx->setFlag("cert-expire", "0"); | |
145 | } | |
146 | ||
120 | 147 | const Error err = ctx->edit(key, std::unique_ptr<EditInteractor> (skei), data); |
121 | 148 | Error ae; |
122 | 149 | const QString log = _detail::audit_log_as_html(ctx, ae); |
142 | 169 | break; |
143 | 170 | } |
144 | 171 | run(std::bind(&sign_key, std::placeholders::_1, key, d->m_userIDsToSign, d->m_checkLevel, d->m_signingKey, |
145 | opts, d->m_dupeOk, d->m_remark, d->m_trustSignature)); | |
172 | opts, d->m_dupeOk, d->m_remark, d->m_trustSignature, d->m_expiration)); | |
146 | 173 | d->m_started = true; |
147 | 174 | return Error(); |
148 | 175 | } |
196 | 223 | d->m_trustSignature = {trust, depth, scope}; |
197 | 224 | } |
198 | 225 | |
226 | void QGpgMESignKeyJob::setExpirationDate(const QDate &expiration) | |
227 | { | |
228 | assert(!d->m_started); | |
229 | d->m_expiration = expiration; | |
230 | } | |
231 | ||
199 | 232 | #include "qgpgmesignkeyjob.moc" |
86 | 86 | /* from SignKeyJob */ |
87 | 87 | void setTrustSignature(GpgME::TrustSignatureTrust trust, unsigned short depth, const QString &scope) Q_DECL_OVERRIDE; |
88 | 88 | |
89 | void setExpirationDate(const QDate &expiration) override; | |
90 | ||
89 | 91 | private: |
90 | 92 | class Private; |
91 | 93 | std::unique_ptr<Private> d; |
45 | 45 | enum class TrustSignatureTrust : char; |
46 | 46 | } |
47 | 47 | |
48 | class QDate; | |
48 | 49 | class QString; |
49 | 50 | |
50 | 51 | namespace QGpgME |
143 | 144 | **/ |
144 | 145 | virtual void setTrustSignature(GpgME::TrustSignatureTrust trust, unsigned short depth, const QString &scope) { Q_UNUSED(trust); Q_UNUSED(depth); Q_UNUSED(scope); }; |
145 | 146 | |
147 | /** | |
148 | * Sets the expiration date of the key signature to @a expiration. By default, | |
149 | * key signatures do not expire. | |
150 | * | |
151 | * Note: Expiration dates after 2106-02-06 will be set to 2106-02-06. | |
152 | * | |
153 | * Not pure virtual for ABI compatibility. | |
154 | **/ | |
155 | virtual void setExpirationDate(const QDate &expiration) { Q_UNUSED(expiration); } | |
156 | ||
146 | 157 | Q_SIGNALS: |
147 | 158 | void result(const GpgME::Error &result, const QString &auditLogAsHtml = QString(), const GpgME::Error &auditLogError = GpgME::Error()); |
148 | 159 | }; |
45 | 45 | #include "dn.h" |
46 | 46 | #include "data.h" |
47 | 47 | #include "dataprovider.h" |
48 | #include "signkeyjob.h" | |
48 | 49 | |
49 | 50 | #include "t-support.h" |
50 | 51 | |
232 | 233 | delete ctx; |
233 | 234 | } |
234 | 235 | |
236 | void testSignKeyWithoutExpiration() | |
237 | { | |
238 | Error err; | |
239 | ||
240 | if (!loopbackSupported()) { | |
241 | return; | |
242 | } | |
243 | ||
244 | auto ctx = Context::create(OpenPGP); | |
245 | QVERIFY(ctx); | |
246 | ||
247 | // Get the signing key (alfa@example.net) | |
248 | auto seckey = ctx->key("A0FF4590BB6122EDEF6E3C542D727CC768697734", err, true); | |
249 | QVERIFY(!err); | |
250 | QVERIFY(!seckey.isNull()); | |
251 | ||
252 | // Get the target key (Bob / Bravo Test) | |
253 | auto target = ctx->key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", err, false); | |
254 | QVERIFY(!err); | |
255 | QVERIFY(!target.isNull()); | |
256 | QVERIFY(target.numUserIDs() > 0); | |
257 | ||
258 | // Create the job | |
259 | auto job = std::unique_ptr<SignKeyJob>{openpgp()->signKeyJob()}; | |
260 | QVERIFY(job); | |
261 | ||
262 | // Hack in the passphrase provider | |
263 | auto jobCtx = Job::context(job.get()); | |
264 | TestPassphraseProvider provider; | |
265 | jobCtx->setPassphraseProvider(&provider); | |
266 | jobCtx->setPinentryMode(Context::PinentryLoopback); | |
267 | ||
268 | // Setup the job | |
269 | job->setExportable(true); | |
270 | job->setSigningKey(seckey); | |
271 | job->setDupeOk(true); | |
272 | ||
273 | connect(job.get(), &SignKeyJob::result, | |
274 | this, [this] (const GpgME::Error &err2, const QString &, const GpgME::Error &) { | |
275 | Q_EMIT asyncDone(); | |
276 | if (err2) { | |
277 | if (err2.code() == GPG_ERR_GENERAL) { | |
278 | QFAIL(qPrintable(QString("The SignKeyJob failed with '%1'.\n" | |
279 | "Hint: Run with GPGMEPP_INTERACTOR_DEBUG=stderr to debug the edit interaction.").arg(err2.asString()))); | |
280 | } else { | |
281 | QFAIL(qPrintable(QString("The SignKeyJob failed with '%1'.").arg(err2.asString()))); | |
282 | } | |
283 | } | |
284 | }); | |
285 | ||
286 | job->start(target); | |
287 | QSignalSpy spy{this, &TestVarious::asyncDone}; | |
288 | QVERIFY(spy.wait(QSIGNALSPY_TIMEOUT)); | |
289 | ||
290 | // At this point the signature should have been added. | |
291 | target.update(); | |
292 | const auto keySignature = target.userID(0).signature(target.userID(0).numSignatures() - 1); | |
293 | QVERIFY(keySignature.neverExpires()); | |
294 | } | |
295 | ||
296 | void testSignKeyWithExpiration() | |
297 | { | |
298 | Error err; | |
299 | ||
300 | if (!loopbackSupported()) { | |
301 | return; | |
302 | } | |
303 | ||
304 | auto ctx = Context::create(OpenPGP); | |
305 | QVERIFY(ctx); | |
306 | ||
307 | // Get the signing key (alfa@example.net) | |
308 | auto seckey = ctx->key("A0FF4590BB6122EDEF6E3C542D727CC768697734", err, true); | |
309 | QVERIFY(!err); | |
310 | QVERIFY(!seckey.isNull()); | |
311 | ||
312 | // Get the target key (Bob / Bravo Test) | |
313 | auto target = ctx->key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", err, false); | |
314 | QVERIFY(!err); | |
315 | QVERIFY(!target.isNull()); | |
316 | QVERIFY(target.numUserIDs() > 0); | |
317 | ||
318 | // Create the job | |
319 | auto job = std::unique_ptr<SignKeyJob>{openpgp()->signKeyJob()}; | |
320 | QVERIFY(job); | |
321 | ||
322 | // Hack in the passphrase provider | |
323 | auto jobCtx = Job::context(job.get()); | |
324 | TestPassphraseProvider provider; | |
325 | jobCtx->setPassphraseProvider(&provider); | |
326 | jobCtx->setPinentryMode(Context::PinentryLoopback); | |
327 | ||
328 | // Setup the job | |
329 | job->setExportable(true); | |
330 | job->setSigningKey(seckey); | |
331 | job->setDupeOk(true); | |
332 | job->setExpirationDate(QDate{2222, 2, 22}); | |
333 | ||
334 | connect(job.get(), &SignKeyJob::result, | |
335 | this, [this] (const GpgME::Error &err2, const QString &, const GpgME::Error &) { | |
336 | Q_EMIT asyncDone(); | |
337 | if (err2) { | |
338 | if (err2.code() == GPG_ERR_GENERAL) { | |
339 | QFAIL(qPrintable(QString("The SignKeyJob failed with '%1'.\n" | |
340 | "Hint: Run with GPGMEPP_INTERACTOR_DEBUG=stderr to debug the edit interaction.").arg(err2.asString()))); | |
341 | } else { | |
342 | QFAIL(qPrintable(QString("The SignKeyJob failed with '%1'.").arg(err2.asString()))); | |
343 | } | |
344 | } | |
345 | }); | |
346 | ||
347 | QTest::ignoreMessage(QtWarningMsg, "Expiration of certification has been changed to QDate(\"2106-02-06\")"); | |
348 | ||
349 | job->start(target); | |
350 | QSignalSpy spy{this, &TestVarious::asyncDone}; | |
351 | QVERIFY(spy.wait(QSIGNALSPY_TIMEOUT)); | |
352 | ||
353 | // At this point the signature should have been added. | |
354 | target.update(); | |
355 | const auto keySignature = target.userID(0).signature(target.userID(0).numSignatures() - 1); | |
356 | QVERIFY(!keySignature.neverExpires()); | |
357 | const auto expirationDate = QDateTime::fromSecsSinceEpoch(keySignature.expirationTime()).date(); | |
358 | QCOMPARE(expirationDate, QDate(2106, 2, 6)); // expiration date is capped at 2106-02-06 | |
359 | } | |
360 | ||
235 | 361 | void testVersion() |
236 | 362 | { |
237 | 363 | QVERIFY(EngineInfo::Version("2.1.0") < EngineInfo::Version("2.1.1")); |
284 | 410 | const QString gpgHome = qgetenv("GNUPGHOME"); |
285 | 411 | QVERIFY(copyKeyrings(gpgHome, mDir.path())); |
286 | 412 | qputenv("GNUPGHOME", mDir.path().toUtf8()); |
413 | QFile conf(mDir.path() + QStringLiteral("/gpg.conf")); | |
414 | QVERIFY(conf.open(QIODevice::WriteOnly)); | |
415 | if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() >= "2.2.18") { | |
416 | conf.write("allow-weak-key-signatures"); | |
417 | } | |
418 | conf.close(); | |
287 | 419 | } |
288 | 420 | |
289 | 421 | private: |