launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22228
[Merge] lp:~cjwatson/launchpad/upload-key-expired-notification into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/upload-key-expired-notification into lp:launchpad.
Commit message:
Send email notifications when an upload is signed with an expired key.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/upload-key-expired-notification/+merge/340530
I think I'd sort of half-thought that https://code.launchpad.net/~cjwatson/launchpad/better-upload-error-notifications/+merge/311179 would cover this case - I certainly intended to consider expired keys as sufficient for notification purposes - but of course expired keys fail signature verification in a different way, so that involves a bit more work.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/upload-key-expired-notification into lp:launchpad.
=== modified file 'lib/lp/archiveuploader/dscfile.py'
--- lib/lp/archiveuploader/dscfile.py 2017-09-17 15:21:22 +0000
+++ lib/lp/archiveuploader/dscfile.py 2018-03-02 17:27:11 +0000
@@ -66,6 +66,7 @@
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.services.encoding import guess as guess_encoding
from lp.services.gpg.interfaces import (
+ GPGKeyExpired,
GPGVerificationError,
IGPGHandler,
)
@@ -143,15 +144,24 @@
if verify_signature:
# We set self.signingkey regardless of whether the key is
- # deactivated, since a deactivated key is still good enough for
- # determining whom to notify, and raising UploadError is enough
- # to prevent the upload being accepted.
- self.signingkey, self.parsed_content = self._verifySignature(
- self.raw_content, self.filepath)
- if not self.signingkey.active:
- raise UploadError("File %s is signed with a deactivated key %s"
- % (self.filepath,
- self.signingkey.fingerprint))
+ # deactivated or expired, since a deactivated or expired key is
+ # still good enough for determining whom to notify, and raising
+ # UploadError is enough to prevent the upload being accepted.
+ try:
+ self.signingkey, self.parsed_content = self._verifySignature(
+ self.raw_content, self.filepath)
+ if not self.signingkey.active:
+ raise UploadError(
+ "File %s is signed with a deactivated key %s" %
+ (self.filepath, self.signingkey.fingerprint))
+ except GPGKeyExpired as e:
+ # This may theoretically return None, but the "expired"
+ # error will take precedence anyway.
+ self.signingkey = getUtility(IGPGKeySet).getByFingerprint(
+ e.key.fingerprint)
+ raise UploadError(
+ "File %s is signed with an expired key %s" %
+ (self.filepath, e.key.fingerprint))
else:
self.logger.debug("%s can be unsigned." % self.filename)
self.parsed_content = self.raw_content
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2018-01-02 16:10:26 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2018-03-02 17:27:11 +0000
@@ -101,7 +101,10 @@
)
from lp.testing.dbuser import switch_dbuser
from lp.testing.fakemethod import FakeMethod
-from lp.testing.gpgkeys import import_public_test_keys
+from lp.testing.gpgkeys import (
+ import_public_key,
+ import_public_test_keys,
+ )
from lp.testing.layers import LaunchpadZopelessLayer
from lp.testing.mail_helpers import pop_notifications
@@ -1997,6 +2000,47 @@
self.assertEmails(expected)
self.assertEqual([], self.oopses)
+ def test_expired_key_upload_sends_mail(self):
+ # An upload signed with an expired key does not OOPS and sends a
+ # rejection email.
+ self.switchToAdmin()
+ email = "expired.key@xxxxxxxxxxxxx"
+ fingerprint = "0DD64D28E5F41138533495200E3DB4D402F53CC6"
+ # This key's email address doesn't exist in sampledata, so we need
+ # to create a person for it and import it.
+ self.factory.makePerson(email=email)
+ import_public_key(email)
+ self.switchToUploader()
+
+ uploadprocessor = self.setupBreezyAndGetUploadProcessor()
+ upload_dir = self.queueUpload("netapplet_1.0-1-expiredkey")
+
+ [result] = self.processUpload(uploadprocessor, upload_dir)
+
+ self.assertEqual(UploadStatusEnum.REJECTED, result)
+ base_contents = [
+ "Subject: [ubuntu] netapplet_1.0-1_source.changes (Rejected)",
+ "File "
+ "%s/netapplet_1.0-1-expiredkey/netapplet_1.0-1_source.changes "
+ "is signed with an expired key %s" % (
+ self.incoming_folder, fingerprint),
+ ]
+ expected = []
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you are the most "
+ "recent person",
+ "listed in this package's changelog."],
+ "recipient": "daniel.silverstone@xxxxxxxxxxxxx",
+ })
+ expected.append({
+ "contents": base_contents + [
+ "You are receiving this email because you made this upload."],
+ "recipient": email,
+ })
+ self.assertEmails(expected)
+ self.assertEqual([], self.oopses)
+
def test_ddeb_upload_overrides(self):
# DDEBs should always be overridden to the same values as their
# counterpart DEB's.
=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
--- lib/lp/archiveuploader/uploadprocessor.py 2017-09-17 10:35:57 +0000
+++ lib/lp/archiveuploader/uploadprocessor.py 2018-03-02 17:27:11 +0000
@@ -397,7 +397,8 @@
notify = False
if upload.is_rejected:
result = UploadStatusEnum.REJECTED
- if upload.changes.parsed_content is not None:
+ if (upload.changes.signingkey is not None or
+ upload.changes.parsed_content is not None):
# We got past the point of checking any required
# signature, so we can do a proper rejection.
upload.do_reject(notify)
=== modified file 'lib/lp/registry/browser/tests/gpg-views.txt'
--- lib/lp/registry/browser/tests/gpg-views.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/registry/browser/tests/gpg-views.txt 2018-03-02 17:27:11 +0000
@@ -26,7 +26,7 @@
... "S\xe9bastien Serre (Bienvenue sous Ubuntu) "
... "<sebastien.serre@xxxxxxxxx> sub 1024g/F39C8D42 2006-08-17")
>>> revoked = "84D2 05F0 3E1E 6709 6CB5 4E26 2BE8 3793 AACC D97C"
- >>> expired = "ECA5 B797 586F 2E27 381A 16CF DE6C 9167 046C 6D63"
+ >>> expired = "0DD6 4D28 E5F4 1138 5334 9520 0E3D B4D4 02F5 3CC6"
>>> def post_fingerprint(fingerprint, action='claim_gpg'):
... request = LaunchpadTestRequest(form={
=== modified file 'lib/lp/registry/model/codeofconduct.py'
--- lib/lp/registry/model/codeofconduct.py 2016-03-14 23:42:45 +0000
+++ lib/lp/registry/model/codeofconduct.py 2018-03-02 17:27:11 +0000
@@ -41,6 +41,7 @@
SQLBase,
)
from lp.services.gpg.interfaces import (
+ GPGKeyExpired,
GPGVerificationError,
IGPGHandler,
)
@@ -270,7 +271,7 @@
try:
sig = gpghandler.getVerifiedSignature(sane_signedcode)
- except GPGVerificationError as e:
+ except (GPGVerificationError, GPGKeyExpired) as e:
return str(e)
if not sig.fingerprint:
=== modified file 'lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt'
--- lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt 2017-07-31 11:45:32 +0000
+++ lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt 2018-03-02 17:27:11 +0000
@@ -20,10 +20,10 @@
Key fingerprint = 84D2 05F0 3E1E 6709 6CB5 4E26 2BE8 3793 AACC D97C
uid Revoked Key <revoked.key@xxxxxxxxxxxxx>
- pub 1024D/046C6D63 2005-10-12 [expired: 2005-10-13]
- Key fingerprint = ECA5 B797 586F 2E27 381A 16CF DE6C 9167 046C 6D63
+ pub 1024D/02F53CC6 2005-10-12 [expires: 2005-10-13]
+ Key fingerprint = 0DD6 4D28 E5F4 1138 5334 9520 0E3D B4D4 02F5 3CC6
uid Expired Key <expired.key@xxxxxxxxxxxxx>
- sub 2048g/D501190D 2005-10-12 [expired: 2005-10-13]
+ sub 1024g/86163BC7 2005-10-12 [expires: 2005-10-13]
Attempts to claim a revoked OpenPGP key fail:
@@ -51,12 +51,12 @@
>>> browser.getControl(
... name='fingerprint').value = (
- ... 'ECA5B797586F2E27381A16CFDE6C9167046C6D63')
+ ... '0DD64D28E5F41138533495200E3DB4D402F53CC6')
>>> browser.getControl('Import Key').click()
>>> for tag in find_tags_by_class(browser.contents, 'error message'):
... print tag.renderContents()
<BLANKLINE>
- The key ECA5B797586F2E27381A16CFDE6C9167046C6D63 cannot be validated
+ The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
because it has expired. Change the expiry date (in a terminal, enter
<kbd>gpg --edit-key <var>your@email.address</var></kbd> then enter
<kbd>expire</kbd>), and try again.
@@ -78,7 +78,7 @@
>>> logintoken = tokenset.new(
... person, 'test@xxxxxxxxxxxxx', 'test@xxxxxxxxxxxxx',
... LoginTokenType.VALIDATEGPG,
- ... 'ECA5B797586F2E27381A16CFDE6C9167046C6D63')
+ ... '0DD64D28E5F41138533495200E3DB4D402F53CC6')
>>> expired_key_token = logintoken.token.encode('ascii')
>>> logout()
@@ -107,7 +107,7 @@
>>> for tag in find_tags_by_class(browser.contents, 'error message'):
... print tag.renderContents()
There is 1 error.
- The key ECA5B797586F2E27381A16CFDE6C9167046C6D63 cannot be validated
+ The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
because it has expired. Change the expiry date (in a terminal, enter
<kbd>gpg --edit-key <var>your@email.address</var></kbd> then enter
<kbd>expire</kbd>), and try again.
=== modified file 'lib/lp/services/gpg/handler.py'
--- lib/lp/services/gpg/handler.py 2018-01-26 22:18:38 +0000
+++ lib/lp/services/gpg/handler.py 2018-03-02 17:27:11 +0000
@@ -133,8 +133,8 @@
"""See IGPGHandler."""
try:
return self.getVerifiedSignature(content, signature)
- except GPGVerificationError:
- # Swallow GPG Verification Errors
+ except (GPGVerificationError, GPGKeyExpired):
+ # Swallow GPG verification errors
pass
return None
@@ -214,18 +214,27 @@
'found multiple signatures')
signature = signatures[0]
+ expired = False
# signature.status == 0 means "Ok"
if signature.status is not None:
- raise GPGVerificationError(signature.status.args)
+ if signature.status.code == gpgme.ERR_KEY_EXPIRED:
+ expired = True
+ else:
+ raise GPGVerificationError(signature.status.args)
- # supporting subkeys by retriving the full key from the
- # keyserver and use the master key fingerprint.
+ # Support subkeys by retrieving the full key from the keyserver and
+ # using the master key fingerprint.
try:
key = self.retrieveKey(signature.fpr)
except GPGKeyNotFoundError:
raise GPGVerificationError(
"Unable to map subkey: %s" % signature.fpr)
+ if expired:
+ # This should already be set, but let's make sure.
+ key.expired = True
+ raise GPGKeyExpired(key)
+
# return the signature container
return PymeSignature(
fingerprint=key.fingerprint,
=== modified file 'lib/lp/services/gpg/interfaces.py'
--- lib/lp/services/gpg/interfaces.py 2017-07-31 11:45:32 +0000
+++ lib/lp/services/gpg/interfaces.py 2018-03-02 17:27:11 +0000
@@ -212,11 +212,10 @@
def getVerifiedSignatureResilient(content, signature=None):
"""Wrapper for getVerifiedSignature.
- It calls the target method exactly 3 times.
-
- Return the result if it succeed during the cycle, otherwise
- capture the errors and emits at the end GPGVerificationError
- with the stored error information.
+ This calls the target method up to three times. Successful results
+ are returned immediately, and GPGKeyExpired errors are raised
+ immediately. Otherwise, captures the errors and raises
+ GPGVerificationError with the accumulated error information.
"""
def getVerifiedSignature(content, signature=None):
@@ -229,13 +228,12 @@
content and signature must be 8-bit encoded str objects. It's up to
the caller to encode or decode as appropriate.
- The only exception likely to be propogated out is GPGVerificationError
-
:param content: The content to be verified as string;
:param signature: The signature as string (or None if content is
clearsigned)
:raise GPGVerificationError: if the signature cannot be verified.
+ :raise GPGKeyExpired: if the signature was made with an expired key.
:return: a `PymeSignature` object.
"""
=== modified file 'lib/lp/services/verification/browser/logintoken.py'
--- lib/lp/services/verification/browser/logintoken.py 2017-07-31 11:45:32 +0000
+++ lib/lp/services/verification/browser/logintoken.py 2018-03-02 17:27:11 +0000
@@ -283,7 +283,7 @@
try:
signature = getUtility(IGPGHandler).getVerifiedSignature(
signedcontent.encode('ASCII'))
- except (GPGVerificationError, UnicodeEncodeError) as e:
+ except (GPGVerificationError, GPGKeyExpired, UnicodeEncodeError) as e:
self.addError(_(
'Launchpad could not verify your signature: ${err}',
mapping=dict(err=str(e))))
=== modified file 'lib/lp/testing/gpgkeys/data/README'
--- lib/lp/testing/gpgkeys/data/README 2011-02-17 13:03:17 +0000
+++ lib/lp/testing/gpgkeys/data/README 2018-03-02 17:27:11 +0000
@@ -1,6 +1,6 @@
-Keys in lib/canonical/launchpad/ftests/gpgkeys should be symlinked to entries
-in lib/lp/testing/keyserver/tests/keys. There should be symlinks for each
+Keys in lib/lp/testing/gpgkeys/data/ should be symlinked to entries in
+lib/lp/testing/keyserver/tests/keys/. There should be symlinks for each
individual subkey ID; for instance, for a regular sign-and-encrypt key there
will be a symlink for the main signing subkey and one for the encryption
subkey.
@@ -16,8 +16,8 @@
- 0xAACCD97C is a revoked GPG key
fingerprint: 84D2 05F0 3E1E 6709 6CB5 4E26 2BE8 3793 AACC D97C
- - 0x046C6D63 is an expired GPG key
- fingerprint: ECA5 B797 586F 2E27 381A 16CF DE6C 9167 046C 6D63
+ - 0x02F53CC6 is an expired GPG key
+ fingerprint: 0DD6 4D28 E5F4 1138 5334 9520 0E3D B4D4 02F5 3CC6
Here's a summary of the key information obtained by piping the files into gpg:
@@ -26,8 +26,8 @@
uid Sample Person <test@xxxxxxxxxxxxx>
sub 1024g/2D28D2AB 2005-04-13
sub 1024D/02BA5EF6 2005-08-01
-pub 1024D/046C6D63 2005-10-12 Expired Key <expired.key@xxxxxxxxxxxxx>
-sub 2048g/D501190D 2005-10-12 [expires: 2005-10-13]
+pub 1024D/02F53CC6 2005-10-12 Expired Key <expired.key@xxxxxxxxxxxxx>
+sub 1024g/86163BC7 2005-10-12 [expires: 2005-10-13]
pub 1024D/17B05A8F 2005-10-12 Sign Only <sign.only@xxxxxxxxxxxxx>
pub 1024D/20687895 2000-05-12 Daniel Silverstone (DOB: 1980-04-09) <dsilvers@xxxxxxxxxxxxxxxxx>
uid Daniel Silverstone <dsilvers@xxxxxxxxxxxxxxxxxxxxxxxxx>
@@ -76,6 +76,8 @@
sub 1024g/2D28D2AB 2005-04-13
sub 1024D/02BA5EF6 2005-08-01
+All secret keys are either passwordless or have the password "test".
+
To look at further information you'll need to gpg --import each key and then
browse them using gpg itself. Good luck!
=== modified file 'lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx'
--- lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx 2005-11-03 01:38:54 +0000
+++ lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx 2018-03-02 17:27:11 +0000
@@ -1,30 +1,25 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: GnuPG v1.4.1 (GNU/Linux)
+Version: GnuPG v1
-mQGiBENM4ncRBAC9202nuoR0vYyr5f9LT89o3ls7LDPSE1oqETC18Do83d59saZR
-+b7fJQtMrwUSGjSkouLlVxqCTyEkj5GMTPdefbuTzZe4aAE+9VlGDmFzFDNGY2I+
-KWSKBfOBMNVIMy59EBDLuGGtRuVDbc/d2nmfvYD8axli+PvN5lIj8J0ezwCg9Mvc
-bnKXRjM/eMUXGTHQTEdwqkcEAKosK/HK/vocN09h4P9BgWB7gGDLzoDinbXMC0xM
-1mtzMLjjTLgl1nOx0chwXqjYJbJkm3+yAskKNLn4tcEU1uuV+IyovR21mgNCRR6Y
-qC+dMeVJPrUrp5WYgPisVxkr+2U0ix66g99e+N3sY7OqPADraInNtrC19hDtIS/E
-eUeLA/9zxMNn6uB/VHXWBI/M6+yEMslPiRKM/h+77BVM6IvQCeimnHDdRNaJZw2m
-XqDUcKEzBxJ8ae93Y0Tq63k8+DAWpY9idoYG89WRFeVp5pCUY3a+NWMhz6Yd1foz
-ZFvq/jSXMf+u2XwPG2qO6qxSIzLqbACeH7IdLihKY0HleM/qXLQnRXhwaXJlZCBL
-ZXkgPGV4cGlyZWQua2V5QGNhbm9uaWNhbC5jb20+iGQEExECACQFAkNM4ncCGwMF
-CQABUYAGCwkIBwMCAxUCAwMWAgECHgECF4AACgkQ3myRZwRsbWMMlQCgtzqW/2C8
-3vellnQs80n/oIVy3/EAoJxtPnraLJxfKTGfZIjares/zp6ZuQINBENM4n8QCAC9
-4skTzwva1T1shicCKGUuDNOmv7MI2hTXlNFJGRnBpKj5gGZV8D3epkN5LgaEVNhu
-mbTbQ2ATMCIeiAtkd3MIBoNXvEvzmNdJQ+iZrwhd8IY/b5Cye4uGQ1KwayaOzrwc
-vNhjMpGzdxRl+v5xWQGAOO4TFFf2t+mlTtZ3d2kGZCr88fWrapT0emFvOa/Frv4P
-Lhrf2ABAv9Qq6HiV6w7ASI6eeXnto2CHxUTflhVP243/94qL5P/df6UW4xNbWXLL
-W3rJ5Ck5pfdVYCxIt+XHA+QNUep6sz0ORLrneqMBS5mmycLysBcwIOR+T6cHReew
-q7RmQUbekZA5v0SrL2ULAAMFB/9ncD2IsB6IFDv+xaz/Ee/cJUbIXp+3g1hbxHk8
-JR52zcsPOoLQMCBgYBchzWnLieiDJTVHFQpmaLE3FTn6pM/COttwxdoincK3T/hW
-HxMjMratYaKoF7s/NeZf//IafX366BjNpHSQw/gpe67EzucmhskTQjFAsCvpu6Qw
-uNBxohAKSU19ljZw6Su5BOx08UqeyF91ENfKvgdsEPedGtev26QTIOdywEjIrkgZ
-F0PU6xuJn5ra5VrpdQ/1o3vLxMtusgjZf8hcO5q2ZeocwD8FJfuMGaPzqpqP+uRi
-IghQ10TOdpihZgQYoNAwDfbpYjN0rz6R4K9Qa8HEbBZOns6tiE8EGBECAA8FAkNM
-4n8CGwwFCQABUYAACgkQ3myRZwRsbWOuYwCggo2UGmIo90M7qufE4WiU3BzU840A
-oOwFrt4h201B0Wvmqyhv2//eNCXP
-=WSfC
+mQGiBENM7LARBACW2NsyVeLIw+Nu3xbTE2mFu5ir04uv5jY3sTiLGFNHeEa8Cfvn
+W/YzLKoFD/5UV3mAJ1tg0cgnVkdyVgsKHI5eywVEjXM+pTlmhktmB9bJIRvFkfnq
+sTEBJD+M7isyM5fDJc9bEXM0qaL8Cslf+XrdAewNsIcEocXqLhhRKPoJWwCg7DQq
+mPdfKwmDqpTF8Me7dfcqP+cD/2qRPwzQK46wsZXPAtCD+THlDIlzz7Ie22tfHZ61
++MDrD0ILJaTmlYSY75zVKkEGtF5mgJb4uEpSy1rCk++x9mGn0XOim7edEBb4YKBo
+C0NXzPhRfRzB9JEM3ISccBUJD2WcrdQgw/NBLdpoxSGtU/x30qGt3tM/kXtmqAgi
+ALS+A/42+zcJt1WksHw2DWpA13zNgHgTzybw4shJD/Vq35ohugCNDNhH/bBdotHU
+VuiaflbLO5p+pjcj2zkYZJhsjHCVtrs7PJLFBVFxWZLycl3RMYK1VmkwXKKYAC+q
+YeMeZHV6MSNHZY825JZ3Amg4J8FvCaLgYiQ/x26kGzWUqlLu17QnRXhwaXJlZCBL
+ZXkgPGV4cGlyZWQua2V5QGNhbm9uaWNhbC5jb20+iGcEExEIACcFAkNM7LACGwMF
+CQABUYAFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQDj201AL1PMZOZwCdFc4D
+ogOrz09OHVPDxiAJioLSJlMAmwdFPlT1uvs8CD7SbSdX5czF2zBruQENBENM7LAQ
+BACEc6ZDHB7bCgChJg97bmf7RbblTNbYEiKrcPAGmwvp/rBDcPPh+PCdjUuJRLh0
+RgZMVYPniXwpEg8HVGNKF7XfThMq9/YSI7kjV4VVHgF9WwO7busnhJqJBj943vGk
++uq9mLTU6nXPyDQHMIRhk6IGu8v4Cr5puMDubbMAQ3125wAEDQP+IEd1UW2w40nB
+w/hcLNleYDhHob688wtULsjrA0w+qpZ0o76LExB4ZgyNpBjJXDzgG/UyAXBsatCP
+nRxvXq3IUa06Up6/1OcuGqhBmEmmpHGmVZvRijIU7jtyCx6BEhjiw2rbhSYlliXI
+q1Fs2FZDUPkFVE+1np/2HyBmsi/fs6mITwQYEQgADwUCQ0zssAIbDAUJAAFRgAAK
+CRAOPbTUAvU8xtW7AJ0QfprHIlPBqTq+/f1WohXyVuNEtwCgy4sRl9hps4Tv9DO6
+ZZ0q9aP5gHE=
+=HVhc
-----END PGP PUBLIC KEY BLOCK-----
=== added file 'lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx'
--- lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx 1970-01-01 00:00:00 +0000
+++ lib/lp/testing/gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx 2018-03-02 17:27:11 +0000
@@ -0,0 +1,26 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1
+
+lQG7BENM7LARBACW2NsyVeLIw+Nu3xbTE2mFu5ir04uv5jY3sTiLGFNHeEa8Cfvn
+W/YzLKoFD/5UV3mAJ1tg0cgnVkdyVgsKHI5eywVEjXM+pTlmhktmB9bJIRvFkfnq
+sTEBJD+M7isyM5fDJc9bEXM0qaL8Cslf+XrdAewNsIcEocXqLhhRKPoJWwCg7DQq
+mPdfKwmDqpTF8Me7dfcqP+cD/2qRPwzQK46wsZXPAtCD+THlDIlzz7Ie22tfHZ61
++MDrD0ILJaTmlYSY75zVKkEGtF5mgJb4uEpSy1rCk++x9mGn0XOim7edEBb4YKBo
+C0NXzPhRfRzB9JEM3ISccBUJD2WcrdQgw/NBLdpoxSGtU/x30qGt3tM/kXtmqAgi
+ALS+A/42+zcJt1WksHw2DWpA13zNgHgTzybw4shJD/Vq35ohugCNDNhH/bBdotHU
+VuiaflbLO5p+pjcj2zkYZJhsjHCVtrs7PJLFBVFxWZLycl3RMYK1VmkwXKKYAC+q
+YeMeZHV6MSNHZY825JZ3Amg4J8FvCaLgYiQ/x26kGzWUqlLu1wAAoLRDAet3qIJU
+Yjz1r/Kx+3UqOMIaCwu0J0V4cGlyZWQgS2V5IDxleHBpcmVkLmtleUBjYW5vbmlj
+YWwuY29tPohnBBMRCAAnBQJDTOywAhsDBQkAAVGABQsJCAcDBRUKCQgLBRYCAwEA
+Ah4BAheAAAoJEA49tNQC9TzGTmcAnRXOA6IDq89PTh1Tw8YgCYqC0iZTAJsHRT5U
+9br7PAg+0m0nV+XMxdswa50BMgRDTOywEAQAhHOmQxwe2woAoSYPe25n+0W25UzW
+2BIiq3DwBpsL6f6wQ3Dz4fjwnY1LiUS4dEYGTFWD54l8KRIPB1RjShe1304TKvf2
+EiO5I1eFVR4BfVsDu27rJ4SaiQY/eN7xpPrqvZi01Op1z8g0BzCEYZOiBrvL+Aq+
+abjA7m2zAEN9ducABA0D/iBHdVFtsONJwcP4XCzZXmA4R6G+vPMLVC7I6wNMPqqW
+dKO+ixMQeGYMjaQYyVw84Bv1MgFwbGrQj50cb16tyFGtOlKev9TnLhqoQZhJpqRx
+plWb0YoyFO47cgsegRIY4sNq24UmJZYlyKtRbNhWQ1D5BVRPtZ6f9h8gZrIv37Op
+AAD5AdZ76oad3qKl+MBtk9L99cUsbVXaIy/CZmU8Y/MD4w8S7IhPBBgRCAAPBQJD
+TOywAhsMBQkAAVGAAAoJEA49tNQC9TzG1bsAoJN1B6mpuipfofKVJlwJpnHKDkRm
+AKCrkRXCIdVurm+LaUXqfLd+KXMraw==
+=EhLl
+-----END PGP PRIVATE KEY BLOCK-----
=== added symlink 'lib/lp/testing/keyserver/tests/keys/0x0DD64D28E5F41138533495200E3DB4D402F53CC6.get'
=== target is u'../../../gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx'
=== removed symlink 'lib/lp/testing/keyserver/tests/keys/0xECA5B797586F2E27381A16CFDE6C9167046C6D63.get'
=== target was u'../../../gpgkeys/data/expired.key@xxxxxxxxxxxxxxxxx'
Follow ups