1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
|
= GPGHandler =
`IGPGHandler` is a utility designed to handle OpenPGP (GPG) operations.
The following operation are supported:
* Importing public and secret keys;
* Generating a new key;
* Finding local keys;
* Retrieving public keys from the keyserver;
* Verifying signatures (see gpg-signatures.txt);
* Encrypting contents (see gpg-encrypt.txt);
* Importing keyring files;
* Obtaining keyserver URLs for public keys;
* Sanitizing fingerprints.
== Importing public OpenPGP keys ==
The importPublicKey method is exposed by IGPGHandler but it's only used
internally by the retrieveKey method. Ideally, we shouldn't need to
check for all error conditions that we do, but we can't assume the
keyserver is a trusted data source, so we have to do that.
>>> from zope.component import getUtility
>>> from canonical.launchpad.webapp.testing import verifyObject
>>> from lp.services.gpg.interfaces import (
... IGPGHandler,
... IPymeKey,
... )
>>> gpghandler = getUtility(IGPGHandler)
-------------------------------------------------------------------------
XXX: All these checks for error conditions should probably be moved to a
unit tests somewhere else at some point. -- Guilherme Salgado, 2006-08-23
-------------------------------------------------------------------------
A GPGKeyNotFoundError is raised if we try to import an empty content.
>>> key = gpghandler.importPublicKey('')
Traceback (most recent call last):
...
GPGKeyNotFoundError...
The same happens for bogus content.
>>> key = gpghandler.importPublicKey('XXXXXXXXX')
Traceback (most recent call last):
...
GPGKeyNotFoundError: ...
Let's recover some coherent data and verify if it works as expected:
>>> import os
>>> from lp.testing.gpgkeys import gpgkeysdir
>>> filepath = os.path.join(gpgkeysdir, 'test@canonical.com.pub')
>>> pubkey = open(filepath).read()
>>> key = gpghandler.importPublicKey(pubkey)
>>> verifyObject(IPymeKey, key)
True
>>> print key.fingerprint
A419AE861E88BC9E04B9C26FBA2B9389DFD20543
>>> print key.secret
False
>>> print key.can_encrypt
True
>>> print key.can_sign
True
>>> print key.can_certify
True
>>> print key.can_authenticate
False
Public keys can be exported in ASCII-armored format.
>>> print key.export()
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----
<BLANKLINE>
Now, try to import a secret key, which will cause a
SecretGPGKeyImportDetected exception to be raised.
>>> filepath = os.path.join(gpgkeysdir, 'test@canonical.com.sec')
>>> seckey = open(filepath).read()
>>> key = gpghandler.importPublicKey(seckey)
Traceback (most recent call last):
...
SecretGPGKeyImportDetected: ...
Now, try to import two public keys, causing a MoreThanOneGPGKeyFound
exception to be raised.
>>> filepath = os.path.join(gpgkeysdir, 'foo.bar@canonical.com.pub')
>>> pubkey2 = open(filepath).read()
>>> key = gpghandler.importPublicKey('\n'.join([pubkey, pubkey2]))
Traceback (most recent call last):
...
MoreThanOneGPGKeyFound: ...
Raise a GPGKeyNotFoundError if we try to import a public key with damaged
preamble.
>>> key = gpghandler.importPublicKey(pubkey[1:])
Traceback (most recent call last):
...
GPGKeyNotFoundError: ...
Apparently GPGME is able to import an incomplete public key:
>>> key = gpghandler.importPublicKey(pubkey[:-300])
>>> assert key is not None
>>> verifyObject(IPymeKey, key)
True
>>> print key.fingerprint
A419AE861E88BC9E04B9C26FBA2B9389DFD20543
But we get an error if the damage is big:
(what probably happened in bug #2547)
>>> key = gpghandler.importPublicKey(pubkey[:-500])
Traceback (most recent call last):
...
GPGKeyNotFoundError: ...
== Importing secret OpenPGP keys ==
Secret keys can be imported using IGPGHandler.importSecretKey() which
does exactly the same job performed by importPublicKey() but
supporting only ASCII-armored secret keys.
>>> filepath = os.path.join(gpgkeysdir, 'test@canonical.com.sec')
>>> seckey = open(filepath).read()
>>> key = gpghandler.importSecretKey(seckey)
>>> verifyObject(IPymeKey, key)
True
>>> print key.fingerprint
A419AE861E88BC9E04B9C26FBA2B9389DFD20543
>>> print key.secret
True
>>> print key.can_encrypt
True
>>> print key.can_sign
True
>>> print key.can_certify
True
>>> print key.can_authenticate
False
Secret keys can be exported in ASCII-armored format.
>>> print key.export()
-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----
<BLANKLINE>
== Generating new keys ==
IGPGHandler support GPG key generation.
We are not testing real key generation because it dependes on machine
entropy that we may not have in PQM or EC2 (to not mention that it
take minutes even when we have). So, the sample key in disk was
generated by running the code below.
We intentionally add some non-ascii charaters in order to check if key
generation and presentation cope with them.
### new_key = gpghandler.generateKey(
### u"Launchpad PPA for Celso \xe1\xe9\xed\xf3\xfa Providelo")
### filepath = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
### export_file = open(filepath, 'w')
### export_file.write(new_key.export())
### export_file.close()
Let's carry on with importing the sampledata key.
>>> filepath = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
>>> seckey = open(filepath).read()
>>> new_key = gpghandler.importSecretKey(seckey)
The secret key is returned. Currently, generateKey() only generates
password-less sign-only keys, i.e. they can sign content but cannot
encrypt.
>>> print new_key.secret
True
>>> print new_key.algorithm
R
>>> print new_key.keysize
1024
>>> print new_key.can_sign
True
>>> print new_key.can_encrypt
False
>>> print new_key.can_certify
True
>>> print new_key.can_authenticate
False
The generated key contains a single UID and only its 'name' term is
set.
>>> [uid] = new_key.uids
>>> print uid.name
Launchpad PPA for Celso áéíóú Providelo
>>> uid.comment
u''
>>> uid.email
u''
The public key is also available.
>>> pub_key = gpghandler.retrieveKey(new_key.fingerprint)
>>> print pub_key.secret
False
>>> print pub_key.algorithm
R
>>> print pub_key.keysize
1024
>>> print pub_key.uids[0].name
Launchpad PPA for Celso áéíóú Providelo
>>> print pub_key.can_encrypt
False
>>> print pub_key.can_sign
True
== Keyserver URLs ==
The gpghandler can also provide us with convenient links to the
keyserver web interface. By default the action is to display the index
page. Notice that the fingerprint must be the 40-byte fingerprint,
to avoid the retrieval of more than one key.
>>> fingerprint = "A419AE861E88BC9E04B9C26FBA2B9389DFD20543"
>>> gpghandler.getURLForKeyInServer(fingerprint)
'http://localhost:11371/pks/lookup?search=0xA419AE861E88BC9E04B9C26FBA2B9389DFD20543&op=index'
But you can also specify your own action:
>>> gpghandler.getURLForKeyInServer(fingerprint, action="get")
'http://localhost:11371/pks/lookup?search=0xA419AE861E88BC9E04B9C26FBA2B9389DFD20543&op=get'
The method accepts a flag to retrieve a link to ubuntu's public
keyserver web interface.
>>> gpghandler.getURLForKeyInServer(fingerprint, public=True)
'http://keyserver.ubuntu.com:11371/pks/lookup?search=0xA419AE861E88BC9E04B9C26FBA2B9389DFD20543&op=index'
== Keyserver uploads ==
IGPGHandler also allow callsites to upload the public part of a local
key to the configuration keyserver.
We will set up and use the test-keyserver.
>>> from lp.testing.keyserver import KeyServerTac
>>> tac = KeyServerTac()
>>> tac.setUp()
Upload the just-generated key to the keyserver so that we can reset
the local keyring.
>>> gpghandler.uploadPublicKey(new_key.fingerprint)
>>> gpghandler.resetLocalState()
>>> len(list(gpghandler.localKeys()))
0
When we we need the public key again we use retrieveKey(), which will
hit the keyserver and import it automatically.
>>> retrieved_key = gpghandler.retrieveKey(new_key.fingerprint)
>>> retrieved_key.fingerprint == new_key.fingerprint
True
An attempt to upload an unknown key will fail.
>>> gpghandler.uploadPublicKey('F' * 40)
Traceback (most recent call last):
...
GPGKeyDoesNotExistOnServer: GPG key
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF does not exist on the keyserver.
Uploading the same key more than once is fine, it is handled on the
keyserver side.
>>> gpghandler.uploadPublicKey(new_key.fingerprint)
An attempt to upload a key when the keyserver is unreachable results
in a error.
>>> tac.tearDown()
>>> gpghandler.uploadPublicKey(new_key.fingerprint)
Traceback (most recent call last):
...
GPGUploadFailure: Could not reach keyserver at
http://localhost:11371...Connection refused...
== Fingerprint sanitizing ==
The GPG handler offers a convenience method to sanitize key
fingerprints:
>>> print gpghandler.sanitizeFingerprint("XXXXX")
None
>>> fingerprint = 'C858 2652 1A6E F6A6 037B B3F7 9FF2 583E 681B 6469'
>>> print gpghandler.sanitizeFingerprint(fingerprint)
C85826521A6EF6A6037BB3F79FF2583E681B6469
>>> fingerprint = 'c858 2652 1a6e f6a6 037b b3f7 9ff2 583e 681b 6469'
>>> print gpghandler.sanitizeFingerprint(fingerprint)
C85826521A6EF6A6037BB3F79FF2583E681B6469
>>> print gpghandler.sanitizeFingerprint('681B 6469')
None
>>> print gpghandler.sanitizeFingerprint('abnckjdiue')
None
>>> non_ascii_chars = u'\xe9\xe1\xed'
>>> fingerprint = ('c858 2652 1a6e f6a6 037b b3f7 9ff2 583e 681b 6469 %s'
... % non_ascii_chars)
>>> print gpghandler.sanitizeFingerprint(fingerprint)
C85826521A6EF6A6037BB3F79FF2583E681B6469
>>> fingerprint = (
... '%s c858 2652 1a6e f6a6 037b b3f7 9ff2 583e 681b 6469 %s'
... % (non_ascii_chars, non_ascii_chars))
>>> print gpghandler.sanitizeFingerprint(fingerprint)
None
|