Merge ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key into launchpad:master

Proposed by Simone Pelosi
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)
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://docs.google.com/document/d/1O7nQTjlASd6T_i3maRJxTBVXfWm-2sPYEwJj-8qm9cY/edit

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) :
review: Needs Fixing
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
Guruprasad (lgp171188) wrote :

Here is the patch that I have mentioned in some of my comments - https://pastebin.ubuntu.com/p/4KFz33gsJM/

review: Needs Fixing
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Guruprasad (lgp171188) wrote :

Thanks for the changes. They look good to me now. 👍

review: Approve
Revision history for this message
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?

Revision history for this message
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.getSigningKeyData()` and `archive.signing_key_fingerprint` are the only ones accessed by external clients and this MP does not change the values returned for these.

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?

Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
William Grant (wgrant) :
Revision history for this message
Simone Pelosi (pelpsi) :
Revision history for this message
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?

Revision history for this message
Guruprasad (lgp171188) :
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
1diff --git a/cronscripts/ppa-update-keys.py b/cronscripts/ppa-update-keys.py
2new file mode 100755
3index 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()
25diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
26index 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 (
137diff --git a/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py b/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py
138index 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
161diff --git a/lib/lp/services/signing/interfaces/signingkey.py b/lib/lp/services/signing/interfaces/signingkey.py
162index 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
187diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py
188index 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 ):
255diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py
256index 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(
330diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py
331index 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
458diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
459index 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
477diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
478index 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.
549diff --git a/lib/lp/soyuz/scripts/ppakeyupdater.py b/lib/lp/soyuz/scripts/ppakeyupdater.py
550new file mode 100644
551index 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!")
612diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py b/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py
613new file mode 100644
614index 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()
1036diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
1037index 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
1177diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
1178index 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,

Subscribers

People subscribed via source and target branches

to status/vote changes: