Merge lp:~cjwatson/launchpad/no-auto-key-retrieve into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18902
Proposed branch: lp:~cjwatson/launchpad/no-auto-key-retrieve
Merge into: lp:launchpad
Diff against target: 163 lines (+54/-34)
2 files modified
lib/lp/services/gpg/doc/gpg-signatures.txt (+10/-4)
lib/lp/services/gpg/handler.py (+44/-30)
To merge this branch: bzr merge lp:~cjwatson/launchpad/no-auto-key-retrieve
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+364100@code.launchpad.net

Commit message

Use our own GPG key retrieval implementation when verifying signatures rather than relying on auto-key-retrieve.

Description of the change

It's hard to verify this in the test suite, but this should avoid a current problem where we sometimes seem to tickle SKS into returning old keys with old expiration dates as a result of auto-key-retrieve using slightly different URLs.

This doesn't quite fix bug 3052 (!), but it should make it easier to fix.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/gpg/doc/gpg-signatures.txt'
2--- lib/lp/services/gpg/doc/gpg-signatures.txt 2012-06-29 08:40:05 +0000
3+++ lib/lp/services/gpg/doc/gpg-signatures.txt 2019-03-12 12:57:49 +0000
4@@ -7,6 +7,10 @@
5
6 >>> transaction.commit()
7
8+ >>> from lp.testing.keyserver import KeyServerTac
9+ >>> keyserver = KeyServerTac()
10+ >>> keyserver.setUp()
11+
12 >>> from lp.testing import login
13 >>> from lp.services.webapp.interfaces import ILaunchBag
14
15@@ -164,7 +168,8 @@
16 >>> gpghandler.getVerifiedSignature(content)
17 Traceback (most recent call last):
18 ...
19- GPGVerificationError: (7, 9, u'No public key')
20+ GPGKeyDoesNotExistOnServer: GPG key E192C0543B1BB2EB does not exist on the
21+ keyserver.
22
23 Due to unpredictable behaviour between the production system and
24 the external keyserver, we have a resilient signature verifier,
25@@ -178,9 +183,9 @@
26 Traceback (most recent call last):
27 ...
28 GPGVerificationError: Verification failed 3 times:
29- ["(7, 9, u'No public key')",
30- "(7, 9, u'No public key')",
31- "(7, 9, u'No public key')"]
32+ ['GPG key E192C0543B1BB2EB does not exist on the keyserver.',
33+ 'GPG key E192C0543B1BB2EB does not exist on the keyserver.',
34+ 'GPG key E192C0543B1BB2EB does not exist on the keyserver.']
35
36
37 Debugging exceptions
38@@ -227,3 +232,4 @@
39 [<gpgme.Signature object at ...>]
40 7
41
42+ >>> keyserver.tearDown()
43
44=== modified file 'lib/lp/services/gpg/handler.py'
45--- lib/lp/services/gpg/handler.py 2018-03-26 14:29:32 +0000
46+++ lib/lp/services/gpg/handler.py 2019-03-12 12:57:49 +0000
47@@ -1,4 +1,4 @@
48-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
49+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
50 # GNU Affero General Public License version 3 (see the file LICENSE).
51
52 __metaclass__ = type
53@@ -99,13 +99,8 @@
54 """
55 self.home = tempfile.mkdtemp(prefix='gpg-')
56 confpath = os.path.join(self.home, 'gpg.conf')
57- # set needed GPG options, 'auto-key-retrieve' is necessary for
58- # automatically retrieve from the keyserver unknown key when
59- # verifying signatures and 'no-auto-check-trustdb' avoid wasting
60- # time verifying the local keyring consistence.
61 with open(confpath, 'w') as conf:
62- conf.write('keyserver hkp://%s\n' % config.gpghandler.host)
63- conf.write('keyserver-options auto-key-retrieve\n')
64+ # Avoid wasting time verifying the local keyring's consistency.
65 conf.write('no-auto-check-trustdb\n')
66 # Prefer a SHA-2 hash where possible, otherwise GPG will fall
67 # back to a hash it can use.
68@@ -157,7 +152,7 @@
69 for i in range(3):
70 try:
71 signature = self.getVerifiedSignature(content, signature)
72- except GPGVerificationError as info:
73+ except GPGKeyNotFoundError as info:
74 errors.append(info)
75 else:
76 return signature
77@@ -167,14 +162,13 @@
78 raise GPGVerificationError(
79 "Verification failed 3 times: %s " % stored_errors)
80
81- def getVerifiedSignature(self, content, signature=None):
82- """See IGPGHandler."""
83-
84- assert not isinstance(content, unicode)
85- assert not isinstance(signature, unicode)
86-
87- ctx = self._getContext()
88-
89+ def _rawVerifySignature(self, ctx, content, signature=None):
90+ """Internals of `getVerifiedSignature`.
91+
92+ This is called twice during a typical verification: once to work out
93+ the correct fingerprint, and once after retrieving the corresponding
94+ key from the keyserver.
95+ """
96 # from `info gpgme` about gpgme_op_verify(SIG, SIGNED_TEXT, PLAIN):
97 #
98 # If SIG is a detached signature, then the signed text should be
99@@ -228,22 +222,42 @@
100 raise GPGVerificationError('Single signature expected, '
101 'found multiple signatures')
102
103- signature = signatures[0]
104+ return plain, signatures[0]
105+
106+ def getVerifiedSignature(self, content, signature=None):
107+ """See IGPGHandler."""
108+
109+ assert not isinstance(content, unicode)
110+ assert not isinstance(signature, unicode)
111+
112+ ctx = self._getContext()
113+
114+ # We may not yet have the public key, so find out the fingerprint we
115+ # need to fetch.
116+ _, sig = self._rawVerifySignature(ctx, content, signature=signature)
117+
118+ # Fetch the full key from the keyserver now that we know its
119+ # fingerprint, and then verify the signature again. (This also lets
120+ # us support subkeys by using the master key fingerprint.)
121+ # XXX cjwatson 2019-03-12: Before GnuPG 2.2.7 and GPGME 1.11.0,
122+ # sig.fpr is a 64-bit key ID in the case where the key isn't in the
123+ # local keyring yet. I haven't yet heard of 64-bit key ID
124+ # collisions in the wild, but even if they happen here,
125+ # importPublicKey will raise MoreThanOneGPGKeyFound, so the worst
126+ # consequence is a denial of service for the owner of an affected
127+ # key. If we do run into this, then the correct fix is to upgrade
128+ # GnuPG and GPGME.
129+ key = self.retrieveKey(sig.fpr)
130+ plain, sig = self._rawVerifySignature(
131+ ctx, content, signature=signature)
132+
133 expired = False
134- # signature.status == 0 means "Ok"
135- if signature.status is not None:
136- if signature.status.code == gpgme.ERR_KEY_EXPIRED:
137+ # sig.status == 0 means "Ok"
138+ if sig.status is not None:
139+ if sig.status.code == gpgme.ERR_KEY_EXPIRED:
140 expired = True
141 else:
142- raise GPGVerificationError(signature.status.args)
143-
144- # Support subkeys by retrieving the full key from the keyserver and
145- # using the master key fingerprint.
146- try:
147- key = self.retrieveKey(signature.fpr)
148- except GPGKeyNotFoundError:
149- raise GPGVerificationError(
150- "Unable to map subkey: %s" % signature.fpr)
151+ raise GPGVerificationError(sig.status.args)
152
153 if expired:
154 # This should already be set, but let's make sure.
155@@ -254,7 +268,7 @@
156 return PymeSignature(
157 fingerprint=key.fingerprint,
158 plain_data=plain.getvalue(),
159- timestamp=signature.timestamp)
160+ timestamp=sig.timestamp)
161
162 def importPublicKey(self, content):
163 """See IGPGHandler."""