launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25238
[Merge] ~cjwatson/launchpad:gpg-inject-generated into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:gpg-inject-generated into launchpad:master.
Commit message:
Allow injecting new GPG keys into signing service
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/389992
This is guarded by the feature rule 'gpg.signing_service.injection.enabled'.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:gpg-inject-generated into launchpad:master.
diff --git a/lib/lp/services/gpg/doc/gpghandler.txt b/lib/lp/services/gpg/doc/gpghandler.txt
index 38fd104..d57bd75 100644
--- a/lib/lp/services/gpg/doc/gpghandler.txt
+++ b/lib/lp/services/gpg/doc/gpghandler.txt
@@ -173,93 +173,6 @@ Secret keys can be exported in ASCII-armored format.
<BLANKLINE>
-== Generating new keys ==
-
-IGPGHandler support GPG key generation.
-
-We are not testing real key generation because it dependes on machine
-entropy that we may not have in PQM or EC2 (to not mention that it
-take minutes even when we have). So, the sample key in disk was
-generated by running the code below.
-
-We intentionally add some non-ascii charaters in order to check if key
-generation and presentation cope with them.
-
- ### new_key = gpghandler.generateKey(
- ### u"Launchpad PPA for Celso \xe1\xe9\xed\xf3\xfa Providelo")
- ### filepath = os.path.join(gpgkeysdir, 'ppa-sample@xxxxxxxxxxxxxxxxx')
- ### export_file = open(filepath, 'w')
- ### export_file.write(new_key.export())
- ### export_file.close()
-
-Let's carry on with importing the sampledata key.
-
- >>> filepath = os.path.join(gpgkeysdir, 'ppa-sample@xxxxxxxxxxxxxxxxx')
- >>> seckey = open(filepath).read()
- >>> new_key = gpghandler.importSecretKey(seckey)
-
-The secret key is returned. Currently, generateKey() only generates
-password-less sign-only keys, i.e. they can sign content but cannot
-encrypt.
-
- >>> print new_key.secret
- True
-
- >>> print new_key.algorithm.title
- R
-
- >>> print new_key.keysize
- 1024
-
- >>> print new_key.can_sign
- True
-
- >>> print new_key.can_encrypt
- False
-
- >>> print new_key.can_certify
- True
-
- >>> print new_key.can_authenticate
- False
-
-The generated key contains a single UID and only its 'name' term is
-set.
-
- >>> [uid] = new_key.uids
-
- >>> print uid.name
- Launchpad PPA for Celso áéíóú Providelo
-
- >>> uid.comment
- u''
-
- >>> uid.email
- u''
-
-The public key is also available.
-
- >>> pub_key = gpghandler.retrieveKey(new_key.fingerprint)
-
- >>> print pub_key.secret
- False
-
- >>> print pub_key.algorithm.title
- R
-
- >>> print pub_key.keysize
- 1024
-
- >>> print pub_key.uids[0].name
- Launchpad PPA for Celso áéíóú Providelo
-
- >>> print pub_key.can_encrypt
- False
-
- >>> print pub_key.can_sign
- True
-
-
== Keyserver uploads ==
IGPGHandler also allow callsites to upload the public part of a local
@@ -271,6 +184,12 @@ We will set up and use the test-keyserver.
>>> tac = KeyServerTac()
>>> tac.setUp()
+Import a test key.
+
+ >>> filepath = os.path.join(gpgkeysdir, 'ppa-sample@xxxxxxxxxxxxxxxxx')
+ >>> seckey = open(filepath).read()
+ >>> new_key = gpghandler.importSecretKey(seckey)
+
Upload the just-generated key to the keyserver so that we can reset
the local keyring.
diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
index e3ac2df..965282a 100644
--- a/lib/lp/services/gpg/handler.py
+++ b/lib/lp/services/gpg/handler.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -12,6 +12,7 @@ __all__ = [
import atexit
from contextlib import contextmanager
+from datetime import datetime
import os
import shutil
import socket
@@ -22,18 +23,22 @@ import tempfile
import gpgme
from lazr.restful.utils import get_current_browser_request
+import pytz
import requests
import six
from six.moves import http_client
from six.moves.urllib.parse import urlencode
+from zope.component import getUtility
from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
from lp.app.validators.email import valid_email
from lp.services.config import config
+from lp.services.features import getFeatureFlag
from lp.services.gpg.interfaces import (
get_gpg_path,
get_gpgme_context,
+ GPG_INJECT,
GPGKeyAlgorithm,
GPGKeyDoesNotExistOnServer,
GPGKeyExpired,
@@ -51,6 +56,8 @@ from lp.services.gpg.interfaces import (
SecretGPGKeyImportDetected,
valid_fingerprint,
)
+from lp.services.signing.enums import SigningKeyType
+from lp.services.signing.interfaces.signingkey import ISigningKeySet
from lp.services.timeline.requesttimeline import get_request_timeline
from lp.services.timeout import (
TimeoutError,
@@ -318,6 +325,15 @@ class GPGHandler:
assert key.exists_in_local_keyring
return key
+ def _injectKeyPair(self, key):
+ """Inject a key pair into the signing service."""
+ secret_key = key.export()
+ public_key = self.retrieveKey(key.fingerprint).export()
+ now = datetime.now().replace(tzinfo=pytz.UTC)
+ getUtility(ISigningKeySet).inject(
+ SigningKeyType.OPENPGP, secret_key, public_key, key.uids[0].name,
+ now)
+
def generateKey(self, name):
"""See `IGPGHandler`."""
context = get_gpgme_context()
@@ -351,6 +367,17 @@ class GPGHandler:
assert key.exists_in_local_keyring, (
'The key does not seem to exist in the local keyring.')
+ if getFeatureFlag(GPG_INJECT):
+ try:
+ self._injectKeyPair(key)
+ except Exception:
+ with gpgme_timeline("delete", key.fingerprint):
+ # For clarity this should be allow_secret=True, but
+ # pygpgme doesn't allow it to be passed as a keyword
+ # argument.
+ context.delete(key.key, True)
+ raise
+
return key
def encryptContent(self, content, key):
diff --git a/lib/lp/services/gpg/interfaces.py b/lib/lp/services/gpg/interfaces.py
index 462c279..78b44c8 100644
--- a/lib/lp/services/gpg/interfaces.py
+++ b/lib/lp/services/gpg/interfaces.py
@@ -1,9 +1,10 @@
-# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
'get_gpg_path',
'get_gpgme_context',
+ 'GPG_INJECT',
'GPGKeyAlgorithm',
'GPGKeyDoesNotExistOnServer',
'GPGKeyExpired',
@@ -38,6 +39,9 @@ from zope.interface import (
)
+GPG_INJECT = 'gpg.signing_service.injection.enabled'
+
+
def valid_fingerprint(fingerprint):
"""Is the fingerprint of valid form."""
# Fingerprints of v3 keys are md5, fingerprints of v4 keys are sha1;
diff --git a/lib/lp/services/gpg/tests/test_gpghandler.py b/lib/lp/services/gpg/tests/test_gpghandler.py
index 7007553..162f39a 100644
--- a/lib/lp/services/gpg/tests/test_gpghandler.py
+++ b/lib/lp/services/gpg/tests/test_gpghandler.py
@@ -1,24 +1,44 @@
-# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
import base64
+from datetime import datetime
import os
import shutil
import subprocess
+from fixtures import (
+ Fixture,
+ MockPatch,
+ )
import gpgme
+import pytz
import responses
+import six
+from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesListwise,
+ MatchesStructure,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
+from lp.services.compat import mock
+from lp.services.features.testing import FeatureFixture
+from lp.services.gpg.handler import signing_only_param
from lp.services.gpg.interfaces import (
get_gpg_path,
+ get_gpgme_context,
+ GPG_INJECT,
GPGKeyDoesNotExistOnServer,
GPGKeyMismatchOnServer,
GPGKeyTemporarilyNotFoundError,
IGPGHandler,
)
from lp.services.log.logger import BufferLogger
+from lp.services.signing.enums import SigningKeyType
+from lp.services.signing.tests.helpers import SigningServiceClientFixture
from lp.services.timeout import TimeoutError
from lp.testing import (
ANONYMOUS,
@@ -27,6 +47,7 @@ from lp.testing import (
TestCase,
)
from lp.testing.gpgkeys import (
+ gpgkeysdir,
import_secret_test_key,
iter_test_key_emails,
test_keyrings,
@@ -37,6 +58,40 @@ from lp.testing.keyserver import KeyServerTac
from lp.testing.layers import LaunchpadFunctionalLayer
+class FakeGenerateKey(Fixture):
+
+ def __init__(self, keyfile):
+ filepath = os.path.join(gpgkeysdir, keyfile)
+ with open(filepath) as f:
+ self.secret_key = f.read()
+
+ def _setUp(self):
+ class GenKeyResult:
+ def __init__(_self, fpr):
+ _self.primary = True
+ _self.sub = False
+ _self.fpr = fpr
+
+ def mock_genkey(params):
+ # Import the key so that it's in the local keyring.
+ imported_key = getUtility(IGPGHandler).importSecretKey(
+ self.secret_key)
+
+ # Fail if the key generation parameters aren't what we expect.
+ expected_params = six.ensure_str(signing_only_param % {
+ "name": imported_key.uids[0].name,
+ })
+ if params != expected_params:
+ raise ValueError(
+ "Got params %r, expected %r" % (params, expected_params))
+ return GenKeyResult(imported_key.fingerprint)
+
+ real_context = get_gpgme_context()
+ mock_context = mock.Mock(wraps=real_context)
+ mock_context.genkey = mock.Mock(side_effect=mock_genkey)
+ self.useFixture(MockPatch("gpgme.Context", return_value=mock_context))
+
+
class TestGPGHandler(TestCase):
"""Unit tests for the GPG handler."""
layer = LaunchpadFunctionalLayer
@@ -307,6 +362,113 @@ class TestGPGHandler(TestCase):
"op=index&search=0x%s" % fingerprint,
self.gpg_handler.getURLForKeyInServer(fingerprint, public=True))
+ def assertGeneratesKey(self):
+ # We don't test real key generation because it depends on machine
+ # entropy that we may not have in buildbot, and in general may be
+ # slow. The sample key on disk was generated by running the code
+ # below.
+ #
+ # We intentionally add some non-ascii characters in order to check
+ # if key generation and presentation cope with them.
+ #
+ # new_key = gpghandler.generateKey(
+ # u"Launchpad PPA for Celso \xe1\xe9\xed\xf3\xfa Providelo")
+ # filepath = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
+ # with open(filepath, "w") as export_file:
+ # export_file.write(new_key.export())
+ self.useFixture(FakeGenerateKey("ppa-sample@xxxxxxxxxxxxxxxxx"))
+ new_key = self.gpg_handler.generateKey(
+ u"Launchpad PPA for Celso \xe1\xe9\xed\xf3\xfa Providelo")
+ # generateKey currently only generates passwordless sign-only keys,
+ # i.e. they can sign content but cannot encrypt. The generated key
+ # contains a single UID and only its "name" term is set.
+ self.assertThat(new_key, MatchesStructure(
+ secret=Is(True),
+ algorithm=MatchesStructure.byEquality(title="R"),
+ keysize=Equals(1024),
+ can_sign=Is(True),
+ can_encrypt=Is(False),
+ can_certify=Is(True),
+ can_authenticate=Is(False),
+ uids=MatchesListwise([
+ MatchesStructure.byEquality(
+ name=(
+ u"Launchpad PPA for Celso "
+ u"\xe1\xe9\xed\xf3\xfa Providelo"),
+ comment=u"",
+ email=u""),
+ ])))
+ # The public key is also available.
+ pub_key = self.gpg_handler.retrieveKey(new_key.fingerprint)
+ self.assertThat(pub_key, MatchesStructure(
+ secret=Is(False),
+ algorithm=MatchesStructure.byEquality(title="R"),
+ keysize=Equals(1024),
+ can_sign=Is(True),
+ can_encrypt=Is(False),
+ can_certify=Is(True),
+ can_authenticate=Is(False),
+ uids=MatchesListwise([
+ MatchesStructure.byEquality(
+ name=(
+ u"Launchpad PPA for Celso "
+ u"\xe1\xe9\xed\xf3\xfa Providelo"),
+ comment=u"",
+ email=u""),
+ ])))
+ return new_key
+
+ def test_generateKey(self):
+ # Generating a key works as expected.
+ signing_service_client = self.useFixture(
+ SigningServiceClientFixture(self.factory))
+ self.assertGeneratesKey()
+ # The gpg.signing_service.injection.enabled feature flag is
+ # disabled, so the key is not injected into the signing service.
+ self.assertEqual(0, signing_service_client.inject.call_count)
+
+ def test_generateKey_injects_key(self):
+ # If the gpg.signing_service.injection.enabled feature flag is
+ # enabled, a generated key is injected into the signing service.
+ signing_service_client = self.useFixture(
+ SigningServiceClientFixture(self.factory))
+ self.useFixture(FeatureFixture({GPG_INJECT: "on"}))
+ now = datetime.now()
+ mock_datetime = self.useFixture(MockPatch(
+ "lp.services.gpg.handler.datetime")).mock
+ mock_datetime.now = lambda: now
+ new_key = self.assertGeneratesKey()
+ new_public_key = self.gpg_handler.retrieveKey(new_key.fingerprint)
+ self.assertEqual(1, signing_service_client.inject.call_count)
+ self.assertThat(
+ signing_service_client.inject.call_args[0],
+ MatchesListwise([
+ Equals(SigningKeyType.OPENPGP),
+ Equals(new_key.export()),
+ Equals(new_public_key.export()),
+ Equals(
+ u"Launchpad PPA for Celso "
+ u"\xe1\xe9\xed\xf3\xfa Providelo"),
+ Equals(now.replace(tzinfo=pytz.UTC)),
+ ]))
+
+ def test_generateKey_handles_key_injection_failure(self):
+ # If the gpg.signing_service.injection.enabled feature flag is
+ # enabled but key injection fails, the GPG handler deletes the
+ # generated key and re-raises the exception raised by the failed
+ # injection.
+ signing_service_client = self.useFixture(
+ SigningServiceClientFixture(self.factory))
+ signing_service_client.inject.side_effect = ValueError("boom")
+ self.useFixture(FeatureFixture({GPG_INJECT: "on"}))
+ self.useFixture(FakeGenerateKey("ppa-sample@xxxxxxxxxxxxxxxxx"))
+ self.assertRaisesWithContent(
+ ValueError, "boom",
+ self.gpg_handler.generateKey,
+ u"Launchpad PPA for Celso \xe1\xe9\xed\xf3\xfa Providelo")
+ self.assertEqual(1, signing_service_client.inject.call_count)
+ self.assertEqual([], list(self.gpg_handler.localKeys()))
+
def test_signContent_uses_sha512_digests(self):
secret_keys = [
("ppa-sample@xxxxxxxxxxxxxxxxx", ""), # 1024R
diff --git a/lib/lp/services/signing/configure.zcml b/lib/lp/services/signing/configure.zcml
index 4b72d7d..b97f96d 100644
--- a/lib/lp/services/signing/configure.zcml
+++ b/lib/lp/services/signing/configure.zcml
@@ -10,6 +10,12 @@
<allow
interface="lp.services.signing.interfaces.signingkey.ISigningKey"/>
</class>
+ <securedutility
+ component="lp.services.signing.model.signingkey.SigningKey"
+ provides="lp.services.signing.interfaces.signingkey.ISigningKeySet">
+ <allow
+ interface="lp.services.signing.interfaces.signingkey.ISigningKeySet"/>
+ </securedutility>
<securedutility
class="lp.services.signing.model.signingkey.ArchiveSigningKeySet"