From d5d915c1a08af380ff8a5088a59d80666bbad4a9 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 5 Feb 2024 19:12:05 +0100 Subject: [PATCH] Add priv key uri support to SignerStore implements option 2 from repository-service-tuf/repository-service-tuf#580 supersedes #427 (does not include a custom "relative file path signer", can be added in a follow-up PR) -- Change `SignerStore.get` to load non-cached signers from private key uri configured on the passed public key in a "x-rstuf-online-key-uri" field. If the public key does not include a uri, RSTUF_KEYVAULT_BACKEND is used as fallback. securesystemslib.signer.CryptoSigner is "registered" to load signers from private key files. No key specific secrets handling is added. This means the keys must be stored unencryped, preferrably using the secrets handling of the deployment platform (e.g. docker secrets). Default schemes in `securesystemslib.signer.SIGNER_FOR_URI_SCHEME` can be used but are untested. **Tests** Add test to load actual signer from private key file. Uses new unencrypted ed25519 private key copied from: secure-systems-lab/securesystemslib@7952c3f Public key stubs in other tests are updated, because signer store now reads the `unrecognized_fields` attribute, which is mandatory in Key objects. -- implements option 2 described in https://github.com/repository-service-tuf/repository-service-tuf/issues/580 mostly supersedes #427 (does not include a custom "relative file path signer") Signed-off-by: Lukas Puehringer --- repository_service_tuf_worker/signer.py | 37 ++++++++++++++---- tests/files/pem/ed25519_private.pem | 3 ++ .../test_signer.py | 39 +++++++++++++++++-- 3 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 tests/files/pem/ed25519_private.pem diff --git a/repository_service_tuf_worker/signer.py b/repository_service_tuf_worker/signer.py index 44af2958..37b1e08b 100644 --- a/repository_service_tuf_worker/signer.py +++ b/repository_service_tuf_worker/signer.py @@ -3,10 +3,21 @@ # SPDX-License-Identifier: MIT from dynaconf import Dynaconf -from securesystemslib.signer import Key, Signer +from securesystemslib.signer import ( + SIGNER_FOR_URI_SCHEME, + CryptoSigner, + Key, + Signer, +) from repository_service_tuf_worker.interfaces import IKeyVault +RSTUF_ONLINE_KEY_URI_FIELD = "x-rstuf-online-key-uri" + +# Register non-default securesystemslib file signer +# secure-systems-lab/securesystemslib#617 +SIGNER_FOR_URI_SCHEME[CryptoSigner.FILE_URI_SCHEME] = CryptoSigner + class SignerStore: """Generic signer store.""" @@ -16,15 +27,25 @@ def __init__(self, settings: Dynaconf): self._signers: dict[str, Signer] = {} def get(self, key: Key) -> Signer: - """Return signer for passed key.""" + """Return signer for passed key. + + - signer is loaded from the uri included in the passed public key + (see SIGNER_FOR_URI_SCHEME for available uri schemes) + - RSTUF_KEYVAULT_BACKEND is used as fallback, if no URI is included + + """ if key.keyid not in self._signers: - vault = self._settings.get("KEYVAULT") - if not isinstance(vault, IKeyVault): - raise ValueError( - "RSTUF_KEYVAULT_BACKEND is required for online signing" - ) + if uri := key.unrecognized_fields.get(RSTUF_ONLINE_KEY_URI_FIELD): + self._signers[key.keyid] = Signer.from_priv_key_uri(uri, key) + + else: + vault = self._settings.get("KEYVAULT") + if not isinstance(vault, IKeyVault): + raise ValueError( + "RSTUF_KEYVAULT_BACKEND is required for online signing" + ) - self._signers[key.keyid] = vault.get(key) + self._signers[key.keyid] = vault.get(key) return self._signers[key.keyid] diff --git a/tests/files/pem/ed25519_private.pem b/tests/files/pem/ed25519_private.pem new file mode 100644 index 00000000..d197b99c --- /dev/null +++ b/tests/files/pem/ed25519_private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGiI3w9x2HZ9UKGi51USN5JN2wtppaYVCRIBTp8ESaj3 +-----END PRIVATE KEY----- diff --git a/tests/unit/tuf_repository_service_worker/test_signer.py b/tests/unit/tuf_repository_service_worker/test_signer.py index 973d3a26..d631b015 100644 --- a/tests/unit/tuf_repository_service_worker/test_signer.py +++ b/tests/unit/tuf_repository_service_worker/test_signer.py @@ -2,11 +2,19 @@ # # SPDX-License-Identifier: MIT +from pathlib import Path + import pytest from pretend import stub +from securesystemslib.signer import CryptoSigner, Key from repository_service_tuf_worker.interfaces import IKeyVault -from repository_service_tuf_worker.signer import SignerStore +from repository_service_tuf_worker.signer import ( + RSTUF_ONLINE_KEY_URI_FIELD, + SignerStore, +) + +_FILES = Path(__file__).parent.parent.parent / "files" class TestSigner: @@ -36,7 +44,7 @@ def get(self, public_key): fake_id = "fake_id" fake_signer = stub() - fake_key = stub(keyid=fake_id) + fake_key = stub(keyid=fake_id, unrecognized_fields={}) fake_settings = stub(get=lambda x: FakeKeyVault()) store = SignerStore(fake_settings) @@ -47,10 +55,35 @@ def get(self, public_key): def test_get_no_vault(self): fake_id = "fake_id" - fake_key = stub(keyid=fake_id) + fake_key = stub(keyid=fake_id, unrecognized_fields={}) fake_settings = stub(get=lambda x: None) store = SignerStore(fake_settings) with pytest.raises(ValueError): store.get(fake_key) + + def test_get_from_file_uri(self): + path = _FILES / "pem" / "ed25519_private.pem" + uri = f"file:{path}?encrypted=false" + + key_metadata = { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": ( + "4f66dabebcf30628963786001984c0b7" + "5c175cdcf3bc4855933a2628f0cd0a0f" + ) + }, + RSTUF_ONLINE_KEY_URI_FIELD: uri, + } + + fake_id = "fake_id" + key = Key.from_dict(fake_id, key_metadata) + + fake_settings = stub() + store = SignerStore(fake_settings) + signer = store.get(key) + + assert isinstance(signer, CryptoSigner)