launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24305
[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