Merge ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key into launchpad:master
- Git
- lp:~pelpsi/launchpad
- create-new-4096-key-for-archives-with-1024-key
- Merge into master
Status: | Merged |
---|---|
Approved by: | Simone Pelosi |
Approved revision: | fa8b67e8fc680fda846ed095954aa93bfc954591 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key |
Merge into: | launchpad:master |
Diff against target: |
1206 lines (+976/-26) 13 files modified
cronscripts/ppa-update-keys.py (+18/-0) lib/lp/archivepublisher/archivegpgsigningkey.py (+86/-1) lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py (+13/-0) lib/lp/services/signing/interfaces/signingkey.py (+15/-0) lib/lp/services/signing/model/signingkey.py (+46/-0) lib/lp/services/signing/tests/test_proxy.py (+26/-20) lib/lp/services/signing/tests/test_signingkey.py (+117/-0) lib/lp/soyuz/interfaces/archive.py (+8/-0) lib/lp/soyuz/model/archive.py (+47/-0) lib/lp/soyuz/scripts/ppakeyupdater.py (+57/-0) lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py (+418/-0) lib/lp/soyuz/tests/test_archive.py (+116/-0) lib/lp/testing/factory.py (+9/-5) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Guruprasad | Approve | ||
Review via email: mp+461648@code.launchpad.net |
Commit message
Add logic to update 1024 PPAs keys
Cronscript to generate new 4096-bit RSA signing keys for the affected
PPAs (the ones with 1024-bit key) and add a row to the signingkey table
with the information about the newly generated key.
The new key will be generated for the default PPA and then propagated
to the other PPAs beloning to the same owner.
Add rows to the archivesigningkey containing updated PPAs
(i.e., one row per signing key-archive combination).
Also add information regarding the new keys to the gpgkey table.
Description of the change
Required for LP130 - ESM authorization for archive snapshots: https:/
Guruprasad (lgp171188) : | # |
Guruprasad (lgp171188) : | # |
Guruprasad (lgp171188) : | # |
Simone Pelosi (pelpsi) : | # |
Guruprasad (lgp171188) : | # |
Simone Pelosi (pelpsi) : | # |
Guruprasad (lgp171188) : | # |
Simone Pelosi (pelpsi) : | # |
Guruprasad (lgp171188) : | # |
Guruprasad (lgp171188) wrote : | # |
Thanks for the changes. They look good to me now. 👍
William Grant (wgrant) wrote : | # |
Thanks, this looks generally good, just a few inline comments.
Is this safe to run today? What will happen when the new key is returned to clients before all the old indexes are resigned?
Guruprasad (lgp171188) wrote : | # |
> Is this safe to run today? What will happen when the new key is returned to clients before all the old indexes are resigned?
Afaik, `archive.
So the old key will continue to be returned. This MP just generates a new 4096-bit RSA key, adds them to the `signingkey`, `gpgkey`, and `archivesigningkey` tables (it also adds an `archivesigningkey` entry for the current 1024-bit RSA key) in preparation for us to update the publisher to start dual-signing archives when an archive has 2 keys (a 1024-bit RSA key and a 4096-bit RSA key).
Does this make sense?
Simone Pelosi (pelpsi) : | # |
William Grant (wgrant) : | # |
Simone Pelosi (pelpsi) : | # |
Guruprasad (lgp171188) wrote : | # |
William,
> Ah, indeed -- does that join mean that rows PPAs with a missing GPGKey row might be skipped by the script entirely? That might be a bigger problem.
Based on the checks that you did for the number of 1024-bit RSA signing keys and the associated archives on the staging database (the SQL query is in a comment of yours in the spec document), we found that all 1024-bit signing keys have rows in the GPGKey table since they were generated on Launchpad itself. Then the default was changed around a decade ago to be 4096-bit RSA keys and all those keys should also have rows in the GPGKey table till we stopped populating it after Launchpad switched to using the signing service a few years ago.
So no archives with a 1024-bit signing key will have a missing GPGKey row and hence none will be skipped by the script.
Does this make sense?
Guruprasad (lgp171188) : | # |
William Grant (wgrant) : | # |
Preview Diff
1 | diff --git a/cronscripts/ppa-update-keys.py b/cronscripts/ppa-update-keys.py |
2 | new file mode 100755 |
3 | index 0000000..50ebef9 |
4 | --- /dev/null |
5 | +++ b/cronscripts/ppa-update-keys.py |
6 | @@ -0,0 +1,18 @@ |
7 | +#!/usr/bin/python3 -S |
8 | +# |
9 | +# Copyright 2024 Canonical Ltd. This software is licensed under the |
10 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
11 | + |
12 | +""" |
13 | +A cron script that generates 4096-bit RSA signing keys for PPAs that only |
14 | +have 1024-bit RSA signing keys. |
15 | +""" |
16 | + |
17 | +import _pythonpath # noqa: F401 |
18 | + |
19 | +from lp.services.config import config |
20 | +from lp.soyuz.scripts.ppakeyupdater import PPAKeyUpdater |
21 | + |
22 | +if __name__ == "__main__": |
23 | + script = PPAKeyUpdater("ppa-generate-keys", config.archivepublisher.dbuser) |
24 | + script.lock_and_run() |
25 | diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py |
26 | index 722cecd..f24322d 100644 |
27 | --- a/lib/lp/archivepublisher/archivegpgsigningkey.py |
28 | +++ b/lib/lp/archivepublisher/archivegpgsigningkey.py |
29 | @@ -30,7 +30,7 @@ from lp.archivepublisher.run_parts import find_run_parts_dir, run_parts |
30 | from lp.registry.interfaces.gpg import IGPGKeySet |
31 | from lp.services.config import config |
32 | from lp.services.features import getFeatureFlag |
33 | -from lp.services.gpg.interfaces import IGPGHandler, IPymeKey |
34 | +from lp.services.gpg.interfaces import GPGKeyAlgorithm, IGPGHandler, IPymeKey |
35 | from lp.services.osutils import remove_if_exists |
36 | from lp.services.propertycache import cachedproperty, get_property_cache |
37 | from lp.services.signing.enums import ( |
38 | @@ -39,6 +39,7 @@ from lp.services.signing.enums import ( |
39 | SigningMode, |
40 | ) |
41 | from lp.services.signing.interfaces.signingkey import ( |
42 | + IArchiveSigningKeySet, |
43 | ISigningKey, |
44 | ISigningKeySet, |
45 | ) |
46 | @@ -330,6 +331,90 @@ class ArchiveGPGSigningKey(SignableArchive): |
47 | signing_key, async_keyserver=async_keyserver |
48 | ) |
49 | |
50 | + def generate4096BitRSASigningKey(self, log=None): |
51 | + """See `IArchiveGPGSigningKey`.""" |
52 | + assert getFeatureFlag( |
53 | + PUBLISHER_GPG_USES_SIGNING_SERVICE |
54 | + ), "Signing service should be enabled to use this feature." |
55 | + assert ( |
56 | + self.archive.signing_key_fingerprint is not None |
57 | + ), "Archive doesn't have an existing signing key to update." |
58 | + current_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
59 | + self.archive.signing_key_fingerprint |
60 | + ) |
61 | + assert ( |
62 | + current_gpg_key.keysize == 1024 |
63 | + ), "Archive already has a 4096-bit RSA signing key." |
64 | + default_ppa = self.archive.owner.archive |
65 | + |
66 | + # If the current signing key is not in the 'archivesigningkey' table, |
67 | + # add it. |
68 | + |
69 | + current_archive_signing_key = getUtility( |
70 | + IArchiveSigningKeySet |
71 | + ).getByArchiveAndFingerprint( |
72 | + self.archive, self.archive.signing_key_fingerprint |
73 | + ) |
74 | + if not current_archive_signing_key: |
75 | + current_signing_key = getUtility(ISigningKeySet).get( |
76 | + SigningKeyType.OPENPGP, self.archive.signing_key_fingerprint |
77 | + ) |
78 | + getUtility(IArchiveSigningKeySet).create( |
79 | + self.archive, None, current_signing_key |
80 | + ) |
81 | + |
82 | + if self.archive != default_ppa: |
83 | + |
84 | + default_ppa_new_signing_key = getUtility( |
85 | + IArchiveSigningKeySet |
86 | + ).get4096BitRSASigningKey(default_ppa) |
87 | + if default_ppa_new_signing_key is None: |
88 | + # Recursively update default_ppa key |
89 | + IArchiveGPGSigningKey( |
90 | + default_ppa |
91 | + ).generate4096BitRSASigningKey(log=log) |
92 | + # Refresh the default_ppa_new_signing_key with |
93 | + # the newly created one. |
94 | + default_ppa_new_signing_key = getUtility( |
95 | + IArchiveSigningKeySet |
96 | + ).get4096BitRSASigningKey(default_ppa) |
97 | + # Propagate the default PPA 4096-bit RSA signing key |
98 | + # to non-default PPAs and return. |
99 | + getUtility(IArchiveSigningKeySet).create( |
100 | + self.archive, None, default_ppa_new_signing_key |
101 | + ) |
102 | + return |
103 | + |
104 | + key_displayname = ( |
105 | + "Launchpad PPA for %s" % self.archive.owner.displayname |
106 | + ) |
107 | + key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard |
108 | + try: |
109 | + signing_key = getUtility(ISigningKeySet).generate( |
110 | + SigningKeyType.OPENPGP, |
111 | + key_displayname, |
112 | + openpgp_key_algorithm=OpenPGPKeyAlgorithm.RSA, |
113 | + length=4096, |
114 | + ) |
115 | + except Exception as e: |
116 | + if log is not None: |
117 | + log.exception( |
118 | + "Error generating signing key for %s: %s %s" |
119 | + % (self.archive.reference, e.__class__.__name__, e) |
120 | + ) |
121 | + raise |
122 | + getUtility(IArchiveSigningKeySet).create( |
123 | + self.archive, None, signing_key |
124 | + ) |
125 | + getUtility(IGPGKeySet).new( |
126 | + key_owner, |
127 | + signing_key.fingerprint[-8:], |
128 | + signing_key.fingerprint, |
129 | + 4096, |
130 | + GPGKeyAlgorithm.R, |
131 | + ) |
132 | + self._uploadPublicSigningKey(signing_key) |
133 | + |
134 | def setSigningKey(self, key_path, async_keyserver=False): |
135 | """See `IArchiveGPGSigningKey`.""" |
136 | assert ( |
137 | diff --git a/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py b/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py |
138 | index d0b03e0..b229caf 100644 |
139 | --- a/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py |
140 | +++ b/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py |
141 | @@ -120,6 +120,19 @@ class IArchiveGPGSigningKey(ISignableArchive): |
142 | upload to the keyserver. |
143 | """ |
144 | |
145 | + def generate4096BitRSASigningKey(log=None): |
146 | + """Generate a 4096-bit RSA signing key for the context archive. |
147 | + |
148 | + :param log: an optional logger. |
149 | + :raises AssertionError: if the context archive doesn't have a |
150 | + signing key. |
151 | + :raises AssertionError: if the context archive already has a |
152 | + 4096-bit RSA signing key. |
153 | + :raises AssertionError: if the signing service is disabled. |
154 | + :raises GPGUploadFailure: if the just-generated key could not be |
155 | + uploaded to the keyserver. |
156 | + """ |
157 | + |
158 | def setSigningKey(key_path, async_keyserver=False): |
159 | """Set a given secret key export as the context archive signing key. |
160 | |
161 | diff --git a/lib/lp/services/signing/interfaces/signingkey.py b/lib/lp/services/signing/interfaces/signingkey.py |
162 | index 50e106d..a99e018 100644 |
163 | --- a/lib/lp/services/signing/interfaces/signingkey.py |
164 | +++ b/lib/lp/services/signing/interfaces/signingkey.py |
165 | @@ -162,6 +162,21 @@ class IArchiveSigningKeySet(Interface): |
166 | :return: The most suitable key, or None. |
167 | """ |
168 | |
169 | + def getByArchiveAndFingerprint(archive, fingerprint): |
170 | + """Get ArchiveSigningKey by archive and fingerprint. |
171 | + |
172 | + :param archive: The archive associated with the ArchiveSigningKey. |
173 | + :param fingerprint: The signing key's fingerprint. |
174 | + :return: The matching ArchiveSigningKey or None. |
175 | + """ |
176 | + |
177 | + def get4096BitRSASigningKey(archive): |
178 | + """Get the 4096-bit RSA SigningKey for the given archive. |
179 | + |
180 | + :param archive: The Archive to search. |
181 | + :return: The matching SigningKey or None. |
182 | + """ |
183 | + |
184 | def getSigningKey(key_type, archive, distro_series, exact_match=False): |
185 | """Get the most suitable SigningKey for a given context. |
186 | |
187 | diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py |
188 | index 126938a..4bb96c3 100644 |
189 | --- a/lib/lp/services/signing/model/signingkey.py |
190 | +++ b/lib/lp/services/signing/model/signingkey.py |
191 | @@ -11,10 +11,12 @@ __all__ = [ |
192 | |
193 | from datetime import timezone |
194 | |
195 | +from storm.expr import Join |
196 | from storm.locals import Bytes, DateTime, Int, Reference, Unicode |
197 | from zope.component import getUtility |
198 | from zope.interface import implementer, provider |
199 | |
200 | +from lp.registry.model.gpgkey import GPGKey |
201 | from lp.services.database.constants import DEFAULT, UTC_NOW |
202 | from lp.services.database.enumcol import DBEnum |
203 | from lp.services.database.interfaces import IPrimaryStore, IStore |
204 | @@ -246,6 +248,50 @@ class ArchiveSigningKeySet: |
205 | ) |
206 | |
207 | @classmethod |
208 | + def getByArchiveAndFingerprint(cls, archive, fingerprint): |
209 | + join = ( |
210 | + ArchiveSigningKey, |
211 | + Join( |
212 | + SigningKey, |
213 | + SigningKey.id == ArchiveSigningKey.signing_key_id, |
214 | + ), |
215 | + ) |
216 | + results = ( |
217 | + IStore(ArchiveSigningKey) |
218 | + .using(*join) |
219 | + .find( |
220 | + ArchiveSigningKey, |
221 | + ArchiveSigningKey.archive == archive, |
222 | + SigningKey.fingerprint == fingerprint, |
223 | + ) |
224 | + ) |
225 | + return results.one() |
226 | + |
227 | + @classmethod |
228 | + def get4096BitRSASigningKey(cls, archive): |
229 | + join = ( |
230 | + ArchiveSigningKey, |
231 | + Join( |
232 | + SigningKey, |
233 | + SigningKey.id == ArchiveSigningKey.signing_key_id, |
234 | + ), |
235 | + Join( |
236 | + GPGKey, |
237 | + GPGKey.fingerprint == SigningKey.fingerprint, |
238 | + ), |
239 | + ) |
240 | + results = ( |
241 | + IStore(ArchiveSigningKey) |
242 | + .using(*join) |
243 | + .find( |
244 | + SigningKey, |
245 | + ArchiveSigningKey.archive == archive, |
246 | + GPGKey.keysize == 4096, |
247 | + ) |
248 | + ) |
249 | + return results.one() |
250 | + |
251 | + @classmethod |
252 | def generate( |
253 | cls, key_type, description, archive, earliest_distro_series=None |
254 | ): |
255 | diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py |
256 | index 6bb14ff..78ba505 100644 |
257 | --- a/lib/lp/services/signing/tests/test_proxy.py |
258 | +++ b/lib/lp/services/signing/tests/test_proxy.py |
259 | @@ -45,7 +45,7 @@ class SigningServiceResponseFactory: |
260 | response.get(url) and response.post(url). See `patch` method. |
261 | """ |
262 | |
263 | - def __init__(self): |
264 | + def __init__(self, fingerprint_generator=None): |
265 | self.service_private_key = PrivateKey.generate() |
266 | self.service_public_key = self.service_private_key.public_key |
267 | self.b64_service_public_key = self.service_public_key.encode( |
268 | @@ -70,6 +70,7 @@ class SigningServiceResponseFactory: |
269 | bytes(self.generated_public_key) |
270 | ).decode("UTF-8") |
271 | self.generated_fingerprint = "338D218488DFD597D8FCB9C328C3E9D9ADA16CEE" |
272 | + self.fingerprint_generator = fingerprint_generator |
273 | |
274 | self.signed_msg_template = b"%d::signed!" |
275 | |
276 | @@ -150,29 +151,34 @@ class SigningServiceResponseFactory: |
277 | status=201, |
278 | ) |
279 | |
280 | - responses.add( |
281 | + def generate_callback(request): |
282 | + fingerprint = self.generated_fingerprint |
283 | + if self.fingerprint_generator: |
284 | + fingerprint = self.fingerprint_generator() |
285 | + data = { |
286 | + "fingerprint": fingerprint, |
287 | + "public-key": self.b64_generated_public_key, |
288 | + } |
289 | + return 201, {}, self._encryptPayload(data, self.response_nonce) |
290 | + |
291 | + responses.add_callback( |
292 | responses.POST, |
293 | self.getUrl("/generate"), |
294 | - body=self._encryptPayload( |
295 | - { |
296 | - "fingerprint": self.generated_fingerprint, |
297 | - "public-key": self.b64_generated_public_key, |
298 | - }, |
299 | - nonce=self.response_nonce, |
300 | - ), |
301 | - status=201, |
302 | + callback=generate_callback, |
303 | ) |
304 | |
305 | - responses.add( |
306 | - responses.POST, |
307 | - self.getUrl("/inject"), |
308 | - body=self._encryptPayload( |
309 | - { |
310 | - "fingerprint": self.generated_fingerprint, |
311 | - }, |
312 | - nonce=self.response_nonce, |
313 | - ), |
314 | - status=200, |
315 | + def inject_callback(request): |
316 | + fingerprint = self.generated_fingerprint |
317 | + if self.fingerprint_generator: |
318 | + fingerprint = self.fingerprint_generator() |
319 | + data = { |
320 | + "fingerprint": fingerprint, |
321 | + "public-key": self.b64_generated_public_key, |
322 | + } |
323 | + return 200, {}, self._encryptPayload(data, self.response_nonce) |
324 | + |
325 | + responses.add_callback( |
326 | + responses.POST, self.getUrl("/inject"), callback=inject_callback |
327 | ) |
328 | |
329 | responses.add( |
330 | diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py |
331 | index 02185dd..327de1e 100644 |
332 | --- a/lib/lp/services/signing/tests/test_signingkey.py |
333 | +++ b/lib/lp/services/signing/tests/test_signingkey.py |
334 | @@ -363,6 +363,123 @@ class TestArchiveSigningKey(TestCaseWithFactory): |
335 | ) |
336 | |
337 | @responses.activate |
338 | + def test_getByArchiveAndFingerprint(self): |
339 | + self.signing_service.addResponses(self) |
340 | + |
341 | + archive = self.factory.makeArchive() |
342 | + distro_series = archive.distribution.series[0] |
343 | + |
344 | + arch_key = getUtility(IArchiveSigningKeySet).generate( |
345 | + SigningKeyType.UEFI, |
346 | + "some description", |
347 | + archive, |
348 | + earliest_distro_series=distro_series, |
349 | + ) |
350 | + |
351 | + store = Store.of(arch_key) |
352 | + store.invalidate() |
353 | + |
354 | + archive_signing_key = getUtility( |
355 | + IArchiveSigningKeySet |
356 | + ).getByArchiveAndFingerprint(archive, arch_key.signing_key.fingerprint) |
357 | + self.assertIsNot(None, archive_signing_key) |
358 | + |
359 | + self.assertThat( |
360 | + archive_signing_key, |
361 | + MatchesStructure.byEquality( |
362 | + key_type=SigningKeyType.UEFI, |
363 | + archive=archive, |
364 | + earliest_distro_series=distro_series, |
365 | + ), |
366 | + ) |
367 | + |
368 | + self.assertThat( |
369 | + archive_signing_key.signing_key, |
370 | + MatchesStructure.byEquality( |
371 | + key_type=SigningKeyType.UEFI, |
372 | + fingerprint=self.signing_service.generated_fingerprint, |
373 | + public_key=bytes(self.signing_service.generated_public_key), |
374 | + ), |
375 | + ) |
376 | + |
377 | + @responses.activate |
378 | + def test_getByArchiveAndFingerprint_wrong_fingerprint(self): |
379 | + self.signing_service.addResponses(self) |
380 | + |
381 | + archive = self.factory.makeArchive() |
382 | + distro_series = archive.distribution.series[0] |
383 | + |
384 | + arch_key = getUtility(IArchiveSigningKeySet).generate( |
385 | + SigningKeyType.UEFI, |
386 | + "some description", |
387 | + archive, |
388 | + earliest_distro_series=distro_series, |
389 | + ) |
390 | + |
391 | + store = Store.of(arch_key) |
392 | + store.invalidate() |
393 | + |
394 | + archive_signing_key = getUtility( |
395 | + IArchiveSigningKeySet |
396 | + ).getByArchiveAndFingerprint(archive, "wrong_fingerprint") |
397 | + self.assertEqual(None, archive_signing_key) |
398 | + |
399 | + @responses.activate |
400 | + def test_get4096BitRSASigningKey(self): |
401 | + self.signing_service.addResponses(self) |
402 | + |
403 | + archive = self.factory.makeArchive() |
404 | + |
405 | + gpg_key = self.factory.makeGPGKey( |
406 | + archive.owner, |
407 | + keysize=4096, |
408 | + ) |
409 | + expected_signing_key = self.factory.makeSigningKey( |
410 | + key_type=SigningKeyType.OPENPGP, fingerprint=gpg_key.fingerprint |
411 | + ) |
412 | + |
413 | + archive_signing_key = getUtility(IArchiveSigningKeySet).create( |
414 | + archive, None, expected_signing_key |
415 | + ) |
416 | + |
417 | + store = Store.of(archive_signing_key) |
418 | + store.invalidate() |
419 | + |
420 | + actual_signing_key = getUtility( |
421 | + IArchiveSigningKeySet |
422 | + ).get4096BitRSASigningKey(archive) |
423 | + |
424 | + self.assertIsNot(None, actual_signing_key) |
425 | + self.assertEqual(expected_signing_key, actual_signing_key) |
426 | + |
427 | + @responses.activate |
428 | + def test_get4096BitRSASigningKey_none(self): |
429 | + self.signing_service.addResponses(self) |
430 | + |
431 | + archive = self.factory.makeArchive() |
432 | + |
433 | + gpg_key = self.factory.makeGPGKey( |
434 | + archive.owner, |
435 | + keysize=1024, |
436 | + ) |
437 | + signing_key = self.factory.makeSigningKey( |
438 | + key_type=SigningKeyType.OPENPGP, fingerprint=gpg_key.fingerprint |
439 | + ) |
440 | + |
441 | + archive_signing_key = getUtility(IArchiveSigningKeySet).create( |
442 | + archive, None, signing_key |
443 | + ) |
444 | + |
445 | + store = Store.of(archive_signing_key) |
446 | + store.invalidate() |
447 | + |
448 | + actual_signing_key = getUtility( |
449 | + IArchiveSigningKeySet |
450 | + ).get4096BitRSASigningKey(archive) |
451 | + |
452 | + self.assertEqual(None, actual_signing_key) |
453 | + |
454 | + @responses.activate |
455 | def test_inject_saves_correctly(self): |
456 | self.signing_service.addResponses(self) |
457 | |
458 | diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py |
459 | index e1521ce..780895a 100644 |
460 | --- a/lib/lp/soyuz/interfaces/archive.py |
461 | +++ b/lib/lp/soyuz/interfaces/archive.py |
462 | @@ -2878,6 +2878,14 @@ class IArchiveSet(Interface): |
463 | :param purpose: Only return archives with this `ArchivePurpose`. |
464 | """ |
465 | |
466 | + def getArchivesWith1024BitRSASigningKey(limit): |
467 | + """Return all archives with only a 1024-bit RSA signing key. |
468 | + |
469 | + The result is ordered by archive id. |
470 | + |
471 | + :param limit: Limit the size of archive result set to this integer. |
472 | + """ |
473 | + |
474 | def getLatestPPASourcePublicationsForDistribution(distribution): |
475 | """The latest 5 PPA source publications for a given distribution. |
476 | |
477 | diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py |
478 | index b86c2e9..f57a931 100644 |
479 | --- a/lib/lp/soyuz/model/archive.py |
480 | +++ b/lib/lp/soyuz/model/archive.py |
481 | @@ -78,6 +78,7 @@ from lp.registry.interfaces.pocket import PackagePublishingPocket |
482 | from lp.registry.interfaces.role import IHasOwner, IPersonRoles |
483 | from lp.registry.interfaces.series import SeriesStatus |
484 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet |
485 | +from lp.registry.model.gpgkey import GPGKey |
486 | from lp.registry.model.sourcepackagename import SourcePackageName |
487 | from lp.registry.model.teammembership import TeamParticipation |
488 | from lp.services.config import config |
489 | @@ -95,6 +96,7 @@ from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent |
490 | from lp.services.propertycache import cachedproperty, get_property_cache |
491 | from lp.services.signing.enums import SigningKeyType |
492 | from lp.services.signing.interfaces.signingkey import ISigningKeySet |
493 | +from lp.services.signing.model.signingkey import ArchiveSigningKey, SigningKey |
494 | from lp.services.tokens import create_token |
495 | from lp.services.webapp.authorization import check_permission |
496 | from lp.services.webapp.interfaces import ILaunchBag |
497 | @@ -3653,6 +3655,51 @@ class ArchiveSet: |
498 | results.order_by(Archive.date_created) |
499 | return results.config(distinct=True) |
500 | |
501 | + def getArchivesWith1024BitRSASigningKey(self, limit): |
502 | + """See `IArchiveSet`.""" |
503 | + join = ( |
504 | + Archive, |
505 | + Join( |
506 | + GPGKey, |
507 | + GPGKey.fingerprint == Archive.signing_key_fingerprint, |
508 | + ), |
509 | + Join( |
510 | + SigningKey, |
511 | + SigningKey.fingerprint == Archive.signing_key_fingerprint, |
512 | + ), |
513 | + ) |
514 | + subquery_join = ( |
515 | + ArchiveSigningKey, |
516 | + Join( |
517 | + SigningKey, |
518 | + SigningKey.id == ArchiveSigningKey.signing_key_id, |
519 | + ), |
520 | + Join( |
521 | + GPGKey, |
522 | + GPGKey.fingerprint == SigningKey.fingerprint, |
523 | + ), |
524 | + ) |
525 | + subquery_results = ( |
526 | + IStore(ArchiveSigningKey) |
527 | + .using(*subquery_join) |
528 | + .find( |
529 | + ArchiveSigningKey.archive_id, |
530 | + GPGKey.keysize == 4096, |
531 | + ) |
532 | + ) |
533 | + results = ( |
534 | + IStore(Archive) |
535 | + .using(*join) |
536 | + .find( |
537 | + Archive, |
538 | + Archive.purpose == ArchivePurpose.PPA, |
539 | + GPGKey.keysize == 1024, |
540 | + Not(Archive.id.is_in(subquery_results)), |
541 | + ) |
542 | + ) |
543 | + results.order_by(Archive.id) |
544 | + return results.config(distinct=True, limit=limit) |
545 | + |
546 | def getLatestPPASourcePublicationsForDistribution(self, distribution): |
547 | """See `IArchiveSet`.""" |
548 | # Circular import. |
549 | diff --git a/lib/lp/soyuz/scripts/ppakeyupdater.py b/lib/lp/soyuz/scripts/ppakeyupdater.py |
550 | new file mode 100644 |
551 | index 0000000..a354652 |
552 | --- /dev/null |
553 | +++ b/lib/lp/soyuz/scripts/ppakeyupdater.py |
554 | @@ -0,0 +1,57 @@ |
555 | +# Copyright 2024 Canonical Ltd. This software is licensed under the |
556 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
557 | + |
558 | +__all__ = [ |
559 | + "PPAKeyUpdater", |
560 | +] |
561 | + |
562 | +from zope.component import getUtility |
563 | + |
564 | +from lp.archivepublisher.interfaces.archivegpgsigningkey import ( |
565 | + IArchiveGPGSigningKey, |
566 | +) |
567 | +from lp.services.scripts.base import LaunchpadCronScript |
568 | +from lp.soyuz.interfaces.archive import IArchiveSet |
569 | + |
570 | + |
571 | +class PPAKeyUpdater(LaunchpadCronScript): |
572 | + usage = "%prog [-L]" |
573 | + description = ( |
574 | + "Generate a new 4096-bit RSA signing key for PPAs with only " |
575 | + "a 1024-bit RSA signing key." |
576 | + ) |
577 | + |
578 | + def add_my_options(self): |
579 | + self.parser.add_option( |
580 | + "-L", |
581 | + "--limit", |
582 | + type=int, |
583 | + help="Number of PPAs to process per run.", |
584 | + ) |
585 | + |
586 | + def generate4096BitRSASigningKey(self, archive): |
587 | + """Generate a new 4096-bit RSA signing key for the given archive.""" |
588 | + self.logger.info( |
589 | + "Generating 4096-bit RSA signing key for %s (%s)" |
590 | + % (archive.reference, archive.displayname) |
591 | + ) |
592 | + archive_signing_key = IArchiveGPGSigningKey(archive) |
593 | + archive_signing_key.generate4096BitRSASigningKey(log=self.logger) |
594 | + |
595 | + def main(self): |
596 | + """ |
597 | + Generate 4096-bit RSA signing keys for the PPAs with only a 1024-bit |
598 | + RSA signing key. |
599 | + """ |
600 | + archive_set = getUtility(IArchiveSet) |
601 | + |
602 | + archives = list( |
603 | + archive_set.getArchivesWith1024BitRSASigningKey(self.options.limit) |
604 | + ) |
605 | + |
606 | + self.logger.info("Archives to update: %s" % (len(archives))) |
607 | + for archive in archives: |
608 | + self.generate4096BitRSASigningKey(archive) |
609 | + self.txn.commit() |
610 | + |
611 | + self.logger.info("Archives updated!") |
612 | diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py b/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py |
613 | new file mode 100644 |
614 | index 0000000..1901169 |
615 | --- /dev/null |
616 | +++ b/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py |
617 | @@ -0,0 +1,418 @@ |
618 | +# Copyright 2024 Canonical Ltd. This software is licensed under the |
619 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
620 | + |
621 | +"""`PPAKeyUpdater` script class tests.""" |
622 | +import random |
623 | + |
624 | +import responses |
625 | +from fixtures.testcase import TestWithFixtures |
626 | +from testtools.testcase import ExpectedException |
627 | +from zope.component import getUtility |
628 | +from zope.security.proxy import removeSecurityProxy |
629 | + |
630 | +from lp.archivepublisher.interfaces.archivegpgsigningkey import ( |
631 | + PUBLISHER_GPG_USES_SIGNING_SERVICE, |
632 | + IArchiveGPGSigningKey, |
633 | +) |
634 | +from lp.registry.interfaces.distribution import IDistributionSet |
635 | +from lp.registry.interfaces.gpg import IGPGKeySet |
636 | +from lp.services.features.testing import FeatureFixture |
637 | +from lp.services.gpg.interfaces import IGPGHandler |
638 | +from lp.services.log.logger import BufferLogger |
639 | +from lp.services.signing.enums import SigningKeyType |
640 | +from lp.services.signing.interfaces.signingkey import IArchiveSigningKeySet |
641 | +from lp.services.signing.interfaces.signingserviceclient import ( |
642 | + ISigningServiceClient, |
643 | +) |
644 | +from lp.services.signing.tests.test_proxy import SigningServiceResponseFactory |
645 | +from lp.soyuz.enums import ArchivePurpose |
646 | +from lp.soyuz.scripts.ppakeyupdater import PPAKeyUpdater |
647 | +from lp.testing import TestCaseWithFactory |
648 | +from lp.testing.faketransaction import FakeTransaction |
649 | +from lp.testing.fixture import ZopeUtilityFixture |
650 | +from lp.testing.layers import LaunchpadZopelessLayer |
651 | + |
652 | + |
653 | +class FakeGPGHandlerSubmitKey: |
654 | + def submitKey(self, content): |
655 | + return |
656 | + |
657 | + |
658 | +def fingerprintGenerator(prefix="4096"): |
659 | + letters = ["A", "B", "C", "D", "E", "F"] |
660 | + return prefix + "".join( |
661 | + random.choice(letters) for _ in range(40 - len(prefix)) |
662 | + ) |
663 | + |
664 | + |
665 | +class TestPPAKeyUpdater(TestCaseWithFactory, TestWithFixtures): |
666 | + layer = LaunchpadZopelessLayer |
667 | + |
668 | + def setUp(self): |
669 | + super().setUp() |
670 | + |
671 | + self.response_factory = SigningServiceResponseFactory( |
672 | + fingerprintGenerator |
673 | + ) |
674 | + |
675 | + client = removeSecurityProxy(getUtility(ISigningServiceClient)) |
676 | + self.addCleanup(client._cleanCaches) |
677 | + self.useFixture( |
678 | + ZopeUtilityFixture(FakeGPGHandlerSubmitKey(), IGPGHandler) |
679 | + ) |
680 | + |
681 | + def makeArchivesWithRSAKey(self, key_size, archives_number=1): |
682 | + archives = [] |
683 | + key_fingerprint = fingerprintGenerator(str(key_size)) |
684 | + owner = self.factory.makePerson() |
685 | + self.factory.makeGPGKey( |
686 | + owner=owner, |
687 | + keyid=key_fingerprint[-8:], |
688 | + fingerprint=key_fingerprint, |
689 | + keysize=key_size, |
690 | + ) |
691 | + self.factory.makeSigningKey( |
692 | + key_type=SigningKeyType.OPENPGP, fingerprint=key_fingerprint |
693 | + ) |
694 | + for _ in range(archives_number): |
695 | + ppa = self.factory.makeArchive( |
696 | + owner=owner, |
697 | + distribution=getUtility(IDistributionSet).getByName( |
698 | + "ubuntutest" |
699 | + ), |
700 | + purpose=ArchivePurpose.PPA, |
701 | + ) |
702 | + ppa.signing_key_fingerprint = key_fingerprint |
703 | + archives.append(ppa) |
704 | + return archives |
705 | + |
706 | + def _getKeyUpdater(self, limit=None, txn=None): |
707 | + """Return a `PPAKeyUpdater` instance. |
708 | + |
709 | + Monkey-patch the script instance with a fake transaction manager. |
710 | + """ |
711 | + test_args = [] |
712 | + if limit: |
713 | + test_args.extend(["-L", limit]) |
714 | + |
715 | + self.logger = BufferLogger() |
716 | + key_generator = PPAKeyUpdater( |
717 | + name="ppa-generate-keys", test_args=test_args, logger=self.logger |
718 | + ) |
719 | + |
720 | + if txn is None: |
721 | + txn = FakeTransaction() |
722 | + key_generator.txn = txn |
723 | + |
724 | + return key_generator |
725 | + |
726 | + def testNoPPAsToUpdate(self): |
727 | + txn = FakeTransaction() |
728 | + key_generator = self._getKeyUpdater(txn=txn) |
729 | + key_generator.main() |
730 | + |
731 | + self.assertIn("Archives to update: 0", self.logger.getLogBuffer()) |
732 | + self.assertEqual(txn.commit_count, 0) |
733 | + |
734 | + def testNoPPAsToUpdate_with_nothing_to_update(self): |
735 | + # Create archives with 4096-bit RSA key. |
736 | + self.makeArchivesWithRSAKey(key_size=4096, archives_number=10) |
737 | + |
738 | + txn = FakeTransaction() |
739 | + key_generator = self._getKeyUpdater(txn=txn) |
740 | + key_generator.main() |
741 | + |
742 | + self.assertIn("Archives to update: 0", self.logger.getLogBuffer()) |
743 | + self.assertEqual(txn.commit_count, 0) |
744 | + |
745 | + @responses.activate |
746 | + def testNoPPAsToUpdate_mixed(self): |
747 | + self.response_factory.addResponses(self) |
748 | + self.useFixture( |
749 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
750 | + ) |
751 | + # Create archives with 4096-bit RSA key. |
752 | + self.makeArchivesWithRSAKey(key_size=4096, archives_number=10) |
753 | + |
754 | + # Create archives with 1024-bit RSA key. |
755 | + self.makeArchivesWithRSAKey(key_size=1024, archives_number=10) |
756 | + |
757 | + # Update the archives with 1024 so that they have both keys. |
758 | + txn = FakeTransaction() |
759 | + key_generator = self._getKeyUpdater(txn=txn) |
760 | + key_generator.main() |
761 | + |
762 | + self.assertIn("Archives to update: 10", self.logger.getLogBuffer()) |
763 | + self.assertEqual(txn.commit_count, 10) |
764 | + |
765 | + # Now we have 10 archives with 4096-bit RSA key |
766 | + # and 10 archives with both 1024-bit and 4096-bit RSA key. |
767 | + txn = FakeTransaction() |
768 | + key_generator = self._getKeyUpdater(txn=txn) |
769 | + key_generator.main() |
770 | + |
771 | + self.assertIn("Archives to update: 0", self.logger.getLogBuffer()) |
772 | + self.assertEqual(txn.commit_count, 0) |
773 | + |
774 | + @responses.activate |
775 | + def testGenerate4096KeyForPPAsWithSameOwner(self): |
776 | + """Test signing key update for PPAs with the same owner. |
777 | + |
778 | + Verify that a new 4096-bit signing key is generated for the |
779 | + default ppa and propagated to the other PPAs. |
780 | + """ |
781 | + self.response_factory.addResponses(self) |
782 | + self.useFixture( |
783 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
784 | + ) |
785 | + archives_number = 3 |
786 | + archives = self.makeArchivesWithRSAKey( |
787 | + key_size=1024, archives_number=archives_number |
788 | + ) |
789 | + |
790 | + # Create archives with 4096-bit RSA key. |
791 | + self.makeArchivesWithRSAKey( |
792 | + key_size=4096, archives_number=archives_number |
793 | + ) |
794 | + |
795 | + txn = FakeTransaction() |
796 | + key_generator = self._getKeyUpdater(txn=txn) |
797 | + key_generator.main() |
798 | + |
799 | + self.assertIn("Archives to update: 3", self.logger.getLogBuffer()) |
800 | + self.assertIn("Archives updated!", self.logger.getLogBuffer()) |
801 | + self.assertEqual(txn.commit_count, archives_number) |
802 | + new_signing_keys = [] |
803 | + for archive in archives: |
804 | + |
805 | + # Check if 4096-bit RSA signing key exists. |
806 | + signing_key = getUtility( |
807 | + IArchiveSigningKeySet |
808 | + ).get4096BitRSASigningKey(archive) |
809 | + self.assertIsNot(None, signing_key) |
810 | + |
811 | + # Retrieve the old 1024-bit RSA signing key using |
812 | + # the archive fingerprint. |
813 | + old_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
814 | + archive.signing_key_fingerprint |
815 | + ) |
816 | + self.assertIsNot(None, old_gpg_key) |
817 | + self.assertEqual(1024, old_gpg_key.keysize) |
818 | + |
819 | + # Check if the new signing key is correctly |
820 | + # added to the `gpgkey` table. |
821 | + new_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
822 | + signing_key.fingerprint |
823 | + ) |
824 | + self.assertIsNot(None, new_gpg_key) |
825 | + self.assertEqual(4096, new_gpg_key.keysize) |
826 | + |
827 | + # Assert that the two keys are different. |
828 | + self.assertIsNot(old_gpg_key, new_gpg_key) |
829 | + new_signing_keys.append(signing_key.fingerprint) |
830 | + |
831 | + # Assert that all the new keys are equal. |
832 | + self.assertEqual(1, len(set(new_signing_keys))) |
833 | + |
834 | + @responses.activate |
835 | + def testGenerate4096KeyForPPAsDifferentOwners(self): |
836 | + """Test signing key update for PPAs with different owners. |
837 | + |
838 | + Verify that a new 4096-bit RSA signing key is generated per |
839 | + archive owner and propagated to all archives of the same owner. |
840 | + """ |
841 | + self.response_factory.addResponses(self) |
842 | + self.useFixture( |
843 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
844 | + ) |
845 | + archives_number = 3 |
846 | + archives = [] |
847 | + for _ in range(archives_number): |
848 | + archives.extend(self.makeArchivesWithRSAKey(key_size=1024)) |
849 | + |
850 | + # Create archives with 4096-bit RSA key. |
851 | + self.makeArchivesWithRSAKey( |
852 | + key_size=4096, archives_number=archives_number |
853 | + ) |
854 | + |
855 | + txn = FakeTransaction() |
856 | + key_generator = self._getKeyUpdater(txn=txn) |
857 | + key_generator.main() |
858 | + |
859 | + self.assertIn("Archives to update: 3", self.logger.getLogBuffer()) |
860 | + self.assertIn("Archives updated!", self.logger.getLogBuffer()) |
861 | + self.assertEqual(txn.commit_count, archives_number) |
862 | + |
863 | + new_signing_keys = [] |
864 | + for archive in archives: |
865 | + |
866 | + # Check if 4096-bit RSA signing key exists. |
867 | + signing_key = getUtility( |
868 | + IArchiveSigningKeySet |
869 | + ).get4096BitRSASigningKey(archive) |
870 | + self.assertIsNot(None, signing_key) |
871 | + |
872 | + # Retrieve the old 1024-bit signing key using |
873 | + # the archive fingerprint. |
874 | + old_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
875 | + archive.signing_key_fingerprint |
876 | + ) |
877 | + self.assertIsNot(None, old_gpg_key) |
878 | + self.assertEqual(1024, old_gpg_key.keysize) |
879 | + |
880 | + # Check if the new signing key is correctly |
881 | + # added to the `gpgkey` table. |
882 | + new_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
883 | + signing_key.fingerprint |
884 | + ) |
885 | + self.assertIsNot(None, new_gpg_key) |
886 | + self.assertEqual(4096, new_gpg_key.keysize) |
887 | + |
888 | + # Assert that the two keys are different. |
889 | + self.assertIsNot(old_gpg_key, new_gpg_key) |
890 | + new_signing_keys.append(signing_key.fingerprint) |
891 | + |
892 | + # Assert that all the keys are different since PPAs |
893 | + # belong to different users. |
894 | + self.assertEqual(len(new_signing_keys), len(set(new_signing_keys))) |
895 | + |
896 | + @responses.activate |
897 | + def testGenerate4096KeyForPPAsLimit(self): |
898 | + """Test limiting the archives to update the key for in a run. |
899 | + |
900 | + Verify that only the specified number of archives are processed. |
901 | + """ |
902 | + self.response_factory.addResponses(self) |
903 | + self.useFixture( |
904 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
905 | + ) |
906 | + archives_number = 3 |
907 | + archives = self.makeArchivesWithRSAKey( |
908 | + key_size=1024, archives_number=archives_number |
909 | + ) |
910 | + |
911 | + txn = FakeTransaction() |
912 | + key_generator = self._getKeyUpdater(txn=txn, limit="2") |
913 | + key_generator.main() |
914 | + |
915 | + # 2/3 PPAs processed. |
916 | + self.assertIn("Archives to update: 2", self.logger.getLogBuffer()) |
917 | + self.assertIn("Archives updated!", self.logger.getLogBuffer()) |
918 | + self.assertEqual(txn.commit_count, 2) |
919 | + |
920 | + new_signing_keys = [] |
921 | + # Check the first 2 archives. |
922 | + for archive in archives[:2]: |
923 | + |
924 | + # Check if 4096-bit RSA signing key exists. |
925 | + signing_key = getUtility( |
926 | + IArchiveSigningKeySet |
927 | + ).get4096BitRSASigningKey(archive) |
928 | + self.assertIsNot(None, signing_key) |
929 | + |
930 | + # Retrieve the old 1024-bit signing key using |
931 | + # the archive fingerprint. |
932 | + old_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
933 | + archive.signing_key_fingerprint |
934 | + ) |
935 | + self.assertIsNot(None, old_gpg_key) |
936 | + self.assertEqual(1024, old_gpg_key.keysize) |
937 | + |
938 | + # Check if the new signing key is correctly |
939 | + # added to the `gpgkey` table. |
940 | + new_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
941 | + signing_key.fingerprint |
942 | + ) |
943 | + self.assertIsNot(None, new_gpg_key) |
944 | + self.assertEqual(4096, new_gpg_key.keysize) |
945 | + |
946 | + # Assert that the two keys are different |
947 | + self.assertIsNot(old_gpg_key, new_gpg_key) |
948 | + new_signing_keys.append(signing_key.fingerprint) |
949 | + |
950 | + key_generator = self._getKeyUpdater(limit="2", txn=txn) |
951 | + key_generator.main() |
952 | + |
953 | + # 3/3 PPAs processed. |
954 | + self.assertIn("Archives to update: 1", self.logger.getLogBuffer()) |
955 | + self.assertIn("Archives updated!", self.logger.getLogBuffer()) |
956 | + self.assertEqual(txn.commit_count, 3) |
957 | + |
958 | + # Check the last archive. |
959 | + for archive in archives[-1:]: |
960 | + |
961 | + # Check if 4096-bit RSA signing key exists. |
962 | + signing_key = getUtility( |
963 | + IArchiveSigningKeySet |
964 | + ).get4096BitRSASigningKey(archive) |
965 | + self.assertIsNot(None, signing_key) |
966 | + |
967 | + # Retrieve the old 1024-bit signing key using |
968 | + # the archive fingerprint. |
969 | + old_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
970 | + archive.signing_key_fingerprint |
971 | + ) |
972 | + self.assertIsNot(None, old_gpg_key) |
973 | + self.assertEqual(1024, old_gpg_key.keysize) |
974 | + |
975 | + # Check if the new signing key is correctly |
976 | + # added to the `gpgkey` table. |
977 | + new_gpg_key = getUtility(IGPGKeySet).getByFingerprint( |
978 | + signing_key.fingerprint |
979 | + ) |
980 | + self.assertIsNot(None, new_gpg_key) |
981 | + self.assertEqual(4096, new_gpg_key.keysize) |
982 | + |
983 | + # Assert that the two keys are different. |
984 | + self.assertIsNot(old_gpg_key, new_gpg_key) |
985 | + new_signing_keys.append(signing_key.fingerprint) |
986 | + |
987 | + self.assertEqual(archives_number, len(new_signing_keys)) |
988 | + |
989 | + def testPPAFingerprintNone(self): |
990 | + """Signing key update for PPA without a key. |
991 | + |
992 | + This should raise an AssertionError. |
993 | + """ |
994 | + self.useFixture( |
995 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
996 | + ) |
997 | + archive = self.factory.makeArchive() |
998 | + archive_signing_key = IArchiveGPGSigningKey(archive) |
999 | + with ExpectedException( |
1000 | + AssertionError, |
1001 | + "Archive doesn't have an existing signing key to update.", |
1002 | + ): |
1003 | + archive_signing_key.generate4096BitRSASigningKey() |
1004 | + |
1005 | + def testPPAAlreadyUpdated(self): |
1006 | + """Signing key update for PPA with a 4096-bit RSA key. |
1007 | + |
1008 | + This should raise an AssertionError. |
1009 | + """ |
1010 | + self.useFixture( |
1011 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}) |
1012 | + ) |
1013 | + archives = self.makeArchivesWithRSAKey(key_size=4096) |
1014 | + archive = archives[0] |
1015 | + archive_signing_key = IArchiveGPGSigningKey(archive) |
1016 | + with ExpectedException( |
1017 | + AssertionError, "Archive already has a 4096-bit RSA signing key." |
1018 | + ): |
1019 | + archive_signing_key.generate4096BitRSASigningKey() |
1020 | + |
1021 | + def testPPAUpdaterNoneFlag(self): |
1022 | + """Signing key update with signing service disabled. |
1023 | + |
1024 | + This should raise an AssertionError. |
1025 | + """ |
1026 | + self.useFixture( |
1027 | + FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: None}) |
1028 | + ) |
1029 | + archive = self.factory.makeArchive() |
1030 | + archive_signing_key = IArchiveGPGSigningKey(archive) |
1031 | + with ExpectedException( |
1032 | + AssertionError, |
1033 | + "Signing service should be enabled to use this feature.", |
1034 | + ): |
1035 | + archive_signing_key.generate4096BitRSASigningKey() |
1036 | diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py |
1037 | index 3d416ac..42a8fba 100644 |
1038 | --- a/lib/lp/soyuz/tests/test_archive.py |
1039 | +++ b/lib/lp/soyuz/tests/test_archive.py |
1040 | @@ -6,6 +6,7 @@ |
1041 | import doctest |
1042 | import http.client |
1043 | import os.path |
1044 | +import random |
1045 | from datetime import date, datetime, timedelta, timezone |
1046 | from pathlib import PurePath |
1047 | from urllib.parse import urlsplit |
1048 | @@ -63,6 +64,7 @@ from lp.services.gpg.interfaces import ( |
1049 | from lp.services.job.interfaces.job import JobStatus |
1050 | from lp.services.macaroons.testing import MacaroonVerifies |
1051 | from lp.services.propertycache import clear_property_cache, get_property_cache |
1052 | +from lp.services.signing.enums import SigningKeyType |
1053 | from lp.services.timeout import default_timeout |
1054 | from lp.services.webapp.authorization import check_permission |
1055 | from lp.services.webapp.interfaces import OAuthPermission |
1056 | @@ -5722,6 +5724,120 @@ class TestArchiveSetGetByReference(TestCaseWithFactory): |
1057 | self.assertLookupFails("~enoent/twonoent/threenoent/fournoent") |
1058 | |
1059 | |
1060 | +class TestArchiveSetGetBy1024BitRSASigningKey(TestCaseWithFactory): |
1061 | + layer = LaunchpadZopelessLayer |
1062 | + |
1063 | + def setUp(self): |
1064 | + super().setUp() |
1065 | + self.set = getUtility(IArchiveSet) |
1066 | + |
1067 | + def makeArchivesWithRSAKey(self, key_size, archives_number=1): |
1068 | + archives = [] |
1069 | + |
1070 | + def fingerprintGenerator(prefix="4096"): |
1071 | + letters = ["A", "B", "C", "D", "E", "F"] |
1072 | + return prefix + "".join( |
1073 | + random.choice(letters) for _ in range(40 - len(prefix)) |
1074 | + ) |
1075 | + |
1076 | + key_fingerprint = fingerprintGenerator(str(key_size)) |
1077 | + owner = self.factory.makePerson() |
1078 | + self.factory.makeGPGKey( |
1079 | + owner=owner, |
1080 | + keyid=key_fingerprint[-8:], |
1081 | + fingerprint=key_fingerprint, |
1082 | + keysize=key_size, |
1083 | + ) |
1084 | + signing_key = self.factory.makeSigningKey( |
1085 | + key_type=SigningKeyType.OPENPGP, fingerprint=key_fingerprint |
1086 | + ) |
1087 | + for _ in range(archives_number): |
1088 | + ppa = self.factory.makeArchive( |
1089 | + owner=owner, |
1090 | + distribution=getUtility(IDistributionSet).getByName( |
1091 | + "ubuntutest" |
1092 | + ), |
1093 | + purpose=ArchivePurpose.PPA, |
1094 | + ) |
1095 | + ppa.signing_key_fingerprint = key_fingerprint |
1096 | + self.factory.makeArchiveSigningKey(ppa, None, signing_key) |
1097 | + archives.append(ppa) |
1098 | + return archives |
1099 | + |
1100 | + def test_no_PPAs_with_1024_bit_key(self): |
1101 | + archives = list( |
1102 | + self.set.getArchivesWith1024BitRSASigningKey(limit=None) |
1103 | + ) |
1104 | + self.assertEqual(0, len(archives)) |
1105 | + |
1106 | + def test_PPAs_with_1024_bit_key(self): |
1107 | + archives_number = 10 |
1108 | + # Create archives with 1024-bit RSA key. |
1109 | + archives = self.makeArchivesWithRSAKey( |
1110 | + key_size=1024, archives_number=archives_number |
1111 | + ) |
1112 | + |
1113 | + actual_archives = list( |
1114 | + getUtility(IArchiveSet).getArchivesWith1024BitRSASigningKey( |
1115 | + limit=None |
1116 | + ) |
1117 | + ) |
1118 | + self.assertEqual(archives_number, len(actual_archives)) |
1119 | + self.assertEqual(archives, actual_archives) |
1120 | + |
1121 | + def test_PPAs_with_1024_bit_key_PPAs_have_4096_bit_key(self): |
1122 | + archives_number = 10 |
1123 | + # Create archives with 1024-bit RSA key. |
1124 | + archives = self.makeArchivesWithRSAKey( |
1125 | + key_size=1024, archives_number=archives_number |
1126 | + ) |
1127 | + |
1128 | + # Create archives with 4096-bit RSA key. |
1129 | + noise_archives = self.makeArchivesWithRSAKey( |
1130 | + key_size=4096, archives_number=5 |
1131 | + ) |
1132 | + |
1133 | + actual_archives = list( |
1134 | + getUtility(IArchiveSet).getArchivesWith1024BitRSASigningKey( |
1135 | + limit=None |
1136 | + ) |
1137 | + ) |
1138 | + self.assertEqual(archives_number, len(actual_archives)) |
1139 | + self.assertEqual(archives, actual_archives) |
1140 | + self.assertNotIn(noise_archives, actual_archives) |
1141 | + |
1142 | + def test_PPAs_with_1024_bit_key_mixed(self): |
1143 | + archives_number = 10 |
1144 | + |
1145 | + owner = self.factory.makePerson() |
1146 | + |
1147 | + # Create archives with 1024-bit RSA key. |
1148 | + archives = self.makeArchivesWithRSAKey( |
1149 | + key_size=1024, archives_number=archives_number |
1150 | + ) |
1151 | + |
1152 | + # Add a 4096-bit RSA key to the archives. |
1153 | + gpg_key = self.factory.makeGPGKey( |
1154 | + owner=owner, |
1155 | + keysize=4096, |
1156 | + ) |
1157 | + signing_key = self.factory.makeSigningKey( |
1158 | + key_type=SigningKeyType.OPENPGP, fingerprint=gpg_key.fingerprint |
1159 | + ) |
1160 | + for archive in archives: |
1161 | + self.factory.makeArchiveSigningKey(archive, None, signing_key) |
1162 | + |
1163 | + # Create archives with 4096-bit RSA key. |
1164 | + self.makeArchivesWithRSAKey(key_size=4096, archives_number=5) |
1165 | + |
1166 | + actual_archives = list( |
1167 | + getUtility(IArchiveSet).getArchivesWith1024BitRSASigningKey( |
1168 | + limit=None |
1169 | + ) |
1170 | + ) |
1171 | + self.assertEqual(0, len(actual_archives)) |
1172 | + |
1173 | + |
1174 | class TestArchiveSetCheckViewPermission(TestCaseWithFactory): |
1175 | layer = DatabaseFunctionalLayer |
1176 | |
1177 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
1178 | index 57d1d45..b4c9bb1 100644 |
1179 | --- a/lib/lp/testing/factory.py |
1180 | +++ b/lib/lp/testing/factory.py |
1181 | @@ -586,16 +586,20 @@ class LaunchpadObjectFactory(ObjectFactory): |
1182 | "SELECT add_test_openid_identifier(%s)", (account.id,) |
1183 | ) |
1184 | |
1185 | - def makeGPGKey(self, owner): |
1186 | + def makeGPGKey(self, owner, fingerprint=None, keyid=None, keysize=None): |
1187 | """Give 'owner' a crappy GPG key for the purposes of testing.""" |
1188 | - key_id = self.getUniqueHexString(digits=8).upper() |
1189 | - fingerprint = key_id + "A" * 32 |
1190 | + if not keyid: |
1191 | + keyid = self.getUniqueHexString(digits=8).upper() |
1192 | + if not fingerprint: |
1193 | + fingerprint = keyid + "A" * 32 |
1194 | + if not keysize: |
1195 | + keysize = self.getUniqueInteger() |
1196 | keyset = getUtility(IGPGKeySet) |
1197 | key = keyset.new( |
1198 | owner, |
1199 | - keyid=key_id, |
1200 | + keyid=keyid, |
1201 | fingerprint=fingerprint, |
1202 | - keysize=self.getUniqueInteger(), |
1203 | + keysize=keysize, |
1204 | algorithm=GPGKeyAlgorithm.R, |
1205 | active=True, |
1206 | can_encrypt=False, |
Here is the patch that I have mentioned in some of my comments - https:/ /pastebin. ubuntu. com/p/4KFz33gsJ M/