← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:lp-signing-inject into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:lp-signing-inject into launchpad:master.

Commit message:
Adding signing service /inject API integrations.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/382457

This MP adds the methods to call lp-signing service, but it's not actually using this just yet. I'm splitting the MP to make it easier to review.

Another MP should come soon to use the `ArchiveSigningKeySet.inject` call to actually import the existing signing keys to the new structure.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:lp-signing-inject into launchpad:master.
diff --git a/lib/lp/services/signing/interfaces/signingkey.py b/lib/lp/services/signing/interfaces/signingkey.py
index 594a053..74d92c9 100644
--- a/lib/lp/services/signing/interfaces/signingkey.py
+++ b/lib/lp/services/signing/interfaces/signingkey.py
@@ -66,8 +66,22 @@ class ISigningKeySet(Interface):
         :param key_type: One of the SigningKeyType enum's value
         :param description: (optional) The description associated with this
                             key
-        :returns: The SigningKey object associated with the newly created
-                  key at lp-signing"""
+        :return: The SigningKey object associated with the newly created
+                 key at lp-signing
+        """
+
+    def inject(key_type, private_key, public_key, description, created_at):
+        """Inject an existing key pair on lp-signing and stores it in LP's
+        database.
+
+        :param key_type: One of the SigningKeyType enum's value.
+        :param private_key: The private key to be injected into lp-signing
+        :param public_key: The public key to be injected into lp-signing
+        :param description: The description of the key being injected
+        :param created_at: The datetime when the key was originally created.
+        :return: The SigningKey object associated with the newly created
+                 key at lp-signing
+        """
 
 
 class IArchiveSigningKey(Interface):
diff --git a/lib/lp/services/signing/interfaces/signingserviceclient.py b/lib/lp/services/signing/interfaces/signingserviceclient.py
index 0d2a242..32ef1c6 100644
--- a/lib/lp/services/signing/interfaces/signingserviceclient.py
+++ b/lib/lp/services/signing/interfaces/signingserviceclient.py
@@ -45,3 +45,13 @@ class ISigningServiceClient(Interface):
         :param mode: SigningMode.ATTACHED or SigningMode.DETACHED
         :return: A dict with 'public-key' and 'signed-message'
         """
+
+    def inject(key_type, private_key, public_key, description, created_at):
+        """Injects an existing key on lp-signing service.
+
+        :param key_type: One of `SigningKeyType` items.
+        :param private_key: The private key content, (bytes or nacl object).
+        :param public_key: The public key content (bytes or nacl object).
+        :param description: The description of this key.
+        :param created_at: datetime of when the key was created.
+        """
diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py
index e77c200..9638466 100644
--- a/lib/lp/services/signing/model/signingkey.py
+++ b/lib/lp/services/signing/model/signingkey.py
@@ -100,6 +100,20 @@ class SigningKey(StormBase):
         store.add(signing_key)
         return signing_key
 
+    @classmethod
+    def inject(cls, key_type, private_key, public_key, description,
+               created_at):
+        signing_service = getUtility(ISigningServiceClient)
+        generated_key = signing_service.inject(
+            key_type, private_key, public_key, description, created_at)
+        signing_key = SigningKey(
+            key_type=key_type, fingerprint=generated_key['fingerprint'],
+            public_key=bytes(public_key),
+            description=description, date_created=created_at)
+        store = IMasterStore(SigningKey)
+        store.add(signing_key)
+        return signing_key
+
     def sign(self, message, message_name):
         if self.key_type in (SigningKeyType.UEFI, SigningKeyType.FIT):
             mode = SigningMode.ATTACHED
diff --git a/lib/lp/services/signing/proxy.py b/lib/lp/services/signing/proxy.py
index d8542e3..7dc248a 100644
--- a/lib/lp/services/signing/proxy.py
+++ b/lib/lp/services/signing/proxy.py
@@ -180,3 +180,24 @@ class SigningServiceClient:
         return {
             'public-key': base64.b64decode(data['public-key']),
             'signed-message': base64.b64decode(data['signed-message'])}
+
+    def inject(self, key_type, private_key, public_key, description,
+               created_at):
+        payload = json.dumps({
+            "key-type": key_type.name,
+            "private-key": base64.b64encode(
+                bytes(private_key)).decode("UTF-8"),
+            "public-key": base64.b64encode(
+                bytes(public_key)).decode("UTF-8"),
+            "created-at": created_at.isoformat(),
+            "description": description,
+        }).encode("UTF-8")
+
+        nonce = self.getNonce()
+        response_nonce = self._makeResponseNonce()
+
+        data = self._requestJson(
+            "/inject", "POST",
+            headers=self._getAuthHeaders(nonce, response_nonce),
+            data=self._encryptPayload(nonce, payload))
+        return {"fingerprint": data["fingerprint"]}
diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py
index a972ea3..9180267 100644
--- a/lib/lp/services/signing/tests/test_proxy.py
+++ b/lib/lp/services/signing/tests/test_proxy.py
@@ -4,6 +4,7 @@
 __metaclass__ = type
 
 import base64
+from datetime import datetime
 import json
 
 from fixtures import MockPatch
@@ -131,8 +132,16 @@ class SigningServiceResponseFactory:
             body=self._encryptPayload({
                 'fingerprint': self.generated_fingerprint,
                 'public-key': self.b64_generated_public_key.decode('utf8')
-                }, nonce=response_nonce),
+            }, nonce=response_nonce),
             status=201)
+
+        responses.add(
+            responses.POST, self.getUrl("/inject"),
+            body=self._encryptPayload({
+                'fingerprint': self.generated_fingerprint,
+            }, nonce=response_nonce),
+            status=200)
+
         call_counts = {'/sign': 0}
 
         def sign_callback(request):
@@ -316,3 +325,45 @@ class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures):
         self.assertEqual(
             bytes(self.response_factory.generated_public_key),
             data['public-key'])
+
+    @responses.activate
+    def test_inject_key(self):
+        """Makes sure that the SigningService.inject method calls the
+        correct endpoints, and actually injects key contents.
+        """
+        self.response_factory.addResponses(self)
+        private_key = PrivateKey.generate()
+        public_key = private_key.public_key
+
+        # Generate the key, and checks if we got back the correct dict.
+        signing = getUtility(ISigningServiceClient)
+        response_data = signing.inject(
+            SigningKeyType.UEFI, private_key, public_key,
+            "This is a test key injected.", datetime.now())
+
+        self.assertEqual(response_data, {
+            'fingerprint': self.response_factory.generated_fingerprint})
+
+        self.assertEqual(3, len(responses.calls))
+
+        # expected order of HTTP calls
+        http_nonce, http_service_key, http_inject = responses.calls
+
+        self.assertEqual("POST", http_nonce.request.method)
+        self.assertEqual(
+            self.response_factory.getUrl("/nonce"), http_nonce.request.url)
+
+        self.assertEqual("GET", http_service_key.request.method)
+        self.assertEqual(
+            self.response_factory.getUrl("/service-key"),
+            http_service_key.request.url)
+
+        self.assertEqual("POST", http_inject.request.method)
+        self.assertEqual(
+            self.response_factory.getUrl("/inject"),
+            http_inject.request.url)
+        self.assertThat(http_inject.request.headers, ContainsDict({
+            "Content-Type": Equals("application/x-boxed-json"),
+            "X-Client-Public-Key": Equals(config.signing.client_public_key),
+            "X-Nonce": Equals(self.response_factory.b64_nonce)}))
+        self.assertIsNotNone(http_inject.request.body)
diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py
index 2e0a5df..c4e7533 100644
--- a/lib/lp/services/signing/tests/test_signingkey.py
+++ b/lib/lp/services/signing/tests/test_signingkey.py
@@ -4,8 +4,11 @@
 __metaclass__ = type
 
 import base64
+from datetime import datetime
 
 from fixtures.testcase import TestWithFixtures
+from nacl.public import PrivateKey
+from pytz import utc
 import responses
 from storm.store import Store
 from testtools.matchers import MatchesStructure
@@ -61,6 +64,33 @@ class TestSigningKey(TestCaseWithFactory, TestWithFixtures):
         self.assertEqual("this is my key", db_key.description)
 
     @responses.activate
+    def test_inject_signing_key_saves_correctly(self):
+        self.signing_service.addResponses(self)
+
+        priv_key = PrivateKey.generate()
+        pub_key = priv_key.public_key
+        created_at = datetime(2020, 4, 16, 16, 35).replace(tzinfo=utc)
+
+        key = SigningKey.inject(
+            SigningKeyType.KMOD, bytes(priv_key), bytes(pub_key),
+            u"This is a test key", created_at)
+        self.assertIsInstance(key, SigningKey)
+
+        store = IMasterStore(SigningKey)
+        store.invalidate()
+
+        rs = store.find(SigningKey)
+        self.assertEqual(1, rs.count())
+        db_key = rs.one()
+
+        self.assertEqual(SigningKeyType.KMOD, db_key.key_type)
+        self.assertEqual(
+            self.signing_service.generated_fingerprint, db_key.fingerprint)
+        self.assertEqual(bytes(pub_key), db_key.public_key)
+        self.assertEqual(u"This is a test key", db_key.description)
+        self.assertEqual(created_at, db_key.date_created)
+
+    @responses.activate
     def test_sign_some_data(self):
         self.signing_service.addResponses(self)