← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ilasc/lp-signing:inject-api into lp-signing:master

 

Ioana Lasc has proposed merging ~ilasc/lp-signing:inject-api into lp-signing:master.

Commit message:
Add injection endpoint

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ilasc/lp-signing/+git/lp-signing/+merge/378734

There are 2 discussion points that I would need to go through before moving any further:
1: What do we want the "created_at" and "updated_at" on the Key object defined in lp_signing/model/key.py to be ?a) the moments the are persisted / updated into lp-signing ?b) the exact replica of the timestamps in LP for this Key - although I'm not sure what they are in LP as I couldn't find anything like that

2: Defining the input to /inject API
It depends what we want to do in LP:a) assemble a Key object as defined in lp_signing/model/key.py and POST that to the /inject endpoint
Notes: For Unit Tests in this case we need to make the Key defined in lp_signing/model/key.py  JSON serializable (it isn't at the moment). Ideally I would think we would want to be able to construct a Key domain object in lp-signing and pass it through post_inject ot the test as if it was POSTed by LP into the endpoint - not possible at the moment and Key is not JSON serializable. 
b) Change the input in the current /inject signature from this:
@validate_body({
    "type": "object",
    "properties": {
        "key-type": {
            "type": "string",
            "enum": [item.token for item in KeyType],
            },
        "lp-key": {"type": "string"},
        },
    "required": ["key-type", "lp-key"],
    })

to this content:
@validate_body({
    "type": "object",
    "properties": {
        "key-type": {
            "type": "string",
            "enum": [item.token for item in KeyType],
            },
        "private-key": {"type": "string"},
        "public-key": {"type": "string"},
        "fingerprint": {"type": "string"},
        },
    "required": ["key-type", "lp-key"],
    })

Depending on answers on 1 and 3 this might expand with timestamps and "LP mirrored flag".

3: Do we need to alter the Key object defined in lp_signing/model/key.py to reflect the fact that this is an "LP mirrored key" or do we not care at this point?
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/lp-signing:inject-api into lp-signing:master.
diff --git a/lp_signing/tests/test_webapi.py b/lp_signing/tests/test_webapi.py
index 617c0a4..0d1b2f8 100644
--- a/lp_signing/tests/test_webapi.py
+++ b/lp_signing/tests/test_webapi.py
@@ -1079,3 +1079,128 @@ class TestSignView(TestCase):
         self.assertThat(
             resp,
             HasAPIError("Signature mode DETACHED not supported with FIT"))
+
+
+class TestInjectView(TestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.fixture = self.useFixture(AppFixture())
+        self.useFixture(DatabaseFixture())
+        self.client = factory.create_client()
+        self.private_key = PrivateKey.generate()
+        self.client.registerPublicKey(self.private_key.public_key)
+        self.nonce = Nonce.generate().nonce
+        store.commit()
+
+    def test_unauthenticated(self):
+        resp = self.fixture.client.post("/inject")
+        self.assertThat(resp, HasAPIError("Request not authenticated"))
+
+    def test_undecodable_client_public_key(self):
+        resp = self.fixture.client.post(
+            "/inject",
+            headers={
+                "X-Client-Public-Key": "nonsense key",
+                "X-Nonce": base64.b64encode(self.nonce).decode("UTF-8"),
+                })
+        self.assertThat(resp, HasAPIError("Cannot decode client public key"))
+
+    def test_undecodable_nonce(self):
+        resp = self.fixture.client.post(
+            "/inject",
+            headers={
+                "X-Client-Public-Key": self.private_key.public_key.encode(
+                    encoder=Base64Encoder).decode("UTF-8"),
+                "X-Nonce": "nonsense nonce",
+                })
+        self.assertThat(resp, HasAPIError("Cannot decode nonce"))
+
+    def test_nonce_already_used(self):
+        Nonce.check(self.nonce)
+        resp = self.fixture.client.post(
+            "/inject", private_key=self.private_key, nonce=self.nonce)
+        self.assertThat(resp, HasAPIError("Invalid nonce"))
+
+    def assertNonceConsumed(self):
+        self.assertRaises(InvalidNonce, Nonce.check, self.nonce)
+
+    def test_unboxable_data(self):
+        resp = self.fixture.client.post(
+            "/inject",
+            headers={
+                "X-Client-Public-Key": self.private_key.public_key.encode(
+                    encoder=Base64Encoder).decode("UTF-8"),
+                "X-Nonce": base64.b64encode(self.nonce).decode("UTF-8"),
+                },
+            data=b"data")
+        self.assertThat(resp, HasAPIError("Authentication failed"))
+        self.assertNonceConsumed()
+
+    def test_unregistered_private_key(self):
+        private_key = PrivateKey.generate()
+        resp = self.fixture.client.post(
+            "/inject", private_key=private_key, nonce=self.nonce)
+        self.assertThat(resp, HasAPIError("Unregistered client public key"))
+        self.assertNonceConsumed()
+
+    def test_wrong_service_public_key(self):
+        private_key = PrivateKey.generate()
+        box = Box(self.private_key, private_key.public_key)
+        message = box.encrypt(b"{}", self.nonce, encoder=Base64Encoder)
+        resp = self.fixture.client.post(
+            "/inject",
+            headers={
+                "X-Client-Public-Key": self.private_key.public_key.encode(
+                    encoder=Base64Encoder).decode("UTF-8"),
+                "X-Nonce": message.nonce.decode("UTF-8"),
+                },
+            data=message.ciphertext)
+        self.assertThat(resp, HasAPIError("Authentication failed"))
+        self.assertNonceConsumed()
+
+    def test_no_json(self):
+        resp = self.fixture.client.post(
+            "/inject", private_key=self.private_key, nonce=self.nonce)
+        self.assertThat(resp, HasAPIError("Error decoding JSON request body"))
+        self.assertNonceConsumed()
+
+    def post_inject(self, json_data=None):
+        return self.fixture.client.post(
+            "/inject", json_data=json_data,
+            private_key=self.private_key, nonce=self.nonce)
+
+    def test_missing_key_type(self):
+        resp = self.post_inject({"lp-key": ""})
+        self.assertThat(
+            resp, HasAPIError("'key-type' is a required property at /"))
+        self.assertNonceConsumed()
+
+    def test_missing_key(self):
+        resp = self.post_inject({"key-type": "UEFI"})
+        self.assertThat(
+            resp, HasAPIError("'lp-key' is a required property at /"))
+        self.assertNonceConsumed()
+
+    def test_invalid_key_type(self):
+        resp = self.post_inject({"key-type": "nonsense", "description": ""})
+        self.assertThat(
+            resp,
+            HasAPIError(
+                "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
+                "'FIT']"))
+        self.assertNonceConsumed()
+
+    def test_inject_uefi(self):
+        # Integration test: inject a real UEFI key and return its
+        key = factory.create_key(key_type=KeyType.UEFI)
+
+        resp = self.post_inject(
+            {
+                "key-type": "UEFI",
+                "private-key": base64.b64encode(
+                    bytes(key.private_key)).decode("UTF-8"),
+                "public-key": base64.b64encode(
+                    bytes(key.public_key)).decode("UTF-8"),
+                "fingerprint": key.fingerprint,
+            })
diff --git a/lp_signing/webapi.py b/lp_signing/webapi.py
index 9bc068c..9103483 100644
--- a/lp_signing/webapi.py
+++ b/lp_signing/webapi.py
@@ -165,3 +165,43 @@ def sign_message():
         "public-key": base64.b64encode(key.public_key).decode("UTF-8"),
         "signed-message": base64.b64encode(signed_message).decode("UTF-8"),
         }, 200
+
+
+inject_api = service.api("/inject", "inject_key", methods=["POST"])
+
+
+@inject_api.view(introduced_at="1.0")
+@validate_body({
+    "type": "object",
+    "properties": {
+        "key-type": {
+            "type": "string",
+            "enum": [item.token for item in KeyType],
+            },
+        "lp-key": {"type": "string"},
+        },
+    "required": ["key-type", "lp-key"],
+    })
+@validate_output({
+    "type": "object",
+    "properties": {
+        "fingerprint": {"type": "string"},
+        "public-key": {"type": "string"},  # base64
+        },
+    "required": ["fingerprint", "public-key"],
+    })
+def inject_key():
+    payload = request.get_json()
+    # key_type = KeyType.items[payload["key-type"]]
+    try:
+        key = base64.b64decode(
+            payload["lp-key"].encode("UTF-8"), validate=True)
+    except binascii.Error:
+        raise DataValidationError.single("Cannot decode message")
+
+    key.addAuthorization(request.client)
+    store.commit()
+    return {
+        "fingerprint": key.fingerprint,
+        "public-key": base64.b64encode(key.public_key).decode("UTF-8"),
+        }, 200

Follow ups