diff --git a/.github/workflows/test-gcp-kms.yml b/.github/workflows/test-gcp-kms.yml new file mode 100644 index 000000000..cce063e44 --- /dev/null +++ b/.github/workflows/test-gcp-kms.yml @@ -0,0 +1,29 @@ + +name: Test GCP KMS + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-20.04 + permissions: + id-token: 'write' + steps: + - uses: 'actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8' + - name: 'Authenticate to Google Cloud' + id: 'auth' + uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72' + with: + token_format: 'access_token' + workload_identity_provider: 'projects/367732848534/locations/global/workloadIdentityPools/securesystemslib-test-pool/providers/securesystemslib-test-provider' + service_account: 'securesystemslib-test@openssf.iam.gserviceaccount.com' + + - name: 'Install google-cloud-kms' + run: pip install google-cloud-kms + + - name: 'Sign with KMSSigner' + run: | + python3 test-signer.py + + diff --git a/securesystemslib/signer.py b/securesystemslib/signer.py index 4b9e6c680..c1500ceb4 100644 --- a/securesystemslib/signer.py +++ b/securesystemslib/signer.py @@ -6,10 +6,23 @@ """ import abc +import logging from typing import Any, Dict, Mapping, Optional import securesystemslib.gpg.functions as gpg +import securesystemslib.hash as sslib_hash import securesystemslib.keys as sslib_keys +from securesystemslib import exceptions + +logger = logging.getLogger(__name__) + +GCP_IMPORT_ERROR = None +try: + from google.cloud import kms +except ImportError: + GCP_IMPORT_ERROR = ( + "google-cloud-kms library required to sign with Google Cloud keys." + ) class Signature: @@ -266,3 +279,36 @@ def sign(self, payload: bytes) -> GPGSignature: sig_dict = gpg.create_signature(payload, self.keyid, self.homedir) return GPGSignature(**sig_dict) + + +class GCPSigner(Signer): + """Google Cloud KMS Signer + + There is no way to input Google Cloud credentials: assumption is that they will + be found in the runtime environment by google.cloud.kms, see + https://cloud.google.com/docs/authentication/getting-started + """ + + def __init__(self, gcp_keyid: str, hash_algo: str, keyid: str): + if GCP_IMPORT_ERROR: + raise exceptions.UnsupportedLibraryError(GCP_IMPORT_ERROR) + + self.gcp_keyid = gcp_keyid + self.hash_algo = hash_algo + self.keyid = keyid + self.client = kms.KeyManagementServiceClient() + + def sign(self, payload: bytes) -> Signature: + # NOTE: request and response can contain CRC32C of the digest/sig: + # This could be useful but would require another dependency... + + hasher = sslib_hash.digest(self.hash_algo) + hasher.update(payload) + digest = {self.hash_algo: hasher.digest()} + request = {"name": self.gcp_keyid, "digest": digest} + + logger.debug("signing request %s", request) + response = self.client.asymmetric_sign(request) + logger.debug("signing response %s", response) + + return Signature(self.keyid, response.signature.hex()) diff --git a/test-signer.py b/test-signer.py new file mode 100644 index 000000000..2edc90922 --- /dev/null +++ b/test-signer.py @@ -0,0 +1,27 @@ +from securesystemslib import keys +from securesystemslib.signer import GCPSigner + +data = "data".encode("utf-8") + +pubkey = { + "keyid": "abcd", + "keytype": "ecdsa", + "scheme": "ecdsa-sha2-nistp256", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDJchWswdXOBpMqXkekAzwuWjL+Hx\ncw2ZonDbixh/wTf1FkpxmT8Aq6/WN6NNXOW4Rw9Lua2aKLZo2ZeNrk2VLA==\n-----END PUBLIC KEY-----\n" + }, +} + + +gcp_id = "projects/openssf/locations/global/keyRings/securesystemslib-test-keyring/cryptoKeys/securesystemslib-test-key/cryptoKeyVersions/1" +# This should be parsed from pubkey +hash_algo = "sha256" + +signer = GCPSigner(gcp_id, hash_algo, pubkey["keyid"]) +sig = signer.sign(data) + +if not keys.verify_signature(pubkey, sig.to_dict(), data): + raise RuntimeError( + f"Failed to verify signature by {pubkey['keyid']}: sig was {sig.to_dict()}" + ) +print("OK")