← Back to team overview

launchpad-reviewers team mailing list archive

[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