Skip to content

Commit

Permalink
Initial GCP Signer implementation
Browse files Browse the repository at this point in the history
Very bare bones Signer for Google Cloud KMS: Private keys live in KMS,
signing happens in KMS (although payloading hash happens in Signer).

This is not super usable without issue secure-systems-lab#447 but demonstrates the simplicity.

Key creation is not supported at this point.

A test is added with a few caveats:
* dependencies are not added to requirements.txt: this would
  more than triple the size of requirements-pinnex.txt...
  Not sure what the best path her is
* Test only works on GitHub (because of the authentication)
* There's a separate tox env, meaning the test only runs once
  per test run: this allows testing separate requirements
  but also makes it easier to set very low usage quotas on GCP
  • Loading branch information
jku committed Nov 2, 2022
1 parent 0b71aaa commit 07ee954
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 1 deletion.
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ jobs:
- python-version: 3.8
os: ubuntu-latest
toxenv: lint
- python-version: 3.x
os: ubuntu-latest
toxenv: kms

runs-on: ${{ matrix.os }}

permissions:
id-token: 'write' # for OIDC auth for GCP authentication

steps:
- name: Checkout securesystemslib
uses: actions/checkout@v2
Expand All @@ -54,11 +60,19 @@ jobs:
# A match with 'restore-keys' is used as fallback
restore-keys: ${{ runner.os }}-pip-

- name: 'Authenticate to Google Cloud'
# Authenticate to GCP KMS, but only if we're running KMS tests
if: ${{ matrix.toxenv == 'kms' }}
uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72'
with:
token_format: 'access_token'
workload_identity_provider: 'projects/843741030650/locations/global/workloadIdentityPools/securesystemslib-tests/providers/securesystemslib-tests'
service_account: '[email protected]'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade tox
- name: Run tox
run: tox -e ${{ matrix.toxenv }}

1 change: 1 addition & 0 deletions requirements-kms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
google-cloud-kms
46 changes: 46 additions & 0 deletions securesystemslib/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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())
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
extras_require={
"crypto": ["cryptography>=37.0.0"],
"pynacl": ["pynacl>1.2.0"],
"gcpkms": ["google-cloud-kms"]
},
packages=find_packages(exclude=["tests", "debian"]),
scripts=[],
Expand Down
56 changes: 56 additions & 0 deletions tests/check_kms_signers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python

"""
This test confirms that signing using Google Cloud KMS works.
Requirements to successfully run it:
* Google Cloud authentication details have to be available in the
environment (see https://github.com/googleapis/python-kms)
* The key defined in the test has to be available to the authenticated
user
Likely the only place where both can be true is the Securesystemslib
GitHub Action environment.
NOTE: the filename is purposefully check_ rather than test_ so that test
discovery doesn't find this unittest and the tests within are only run
when explicitly invoked.
"""

import unittest

from securesystemslib.signer import GCPSigner
from securesystemslib import keys

class TestKMSKeys(unittest.TestCase):
"""Test that KMS keys can be used to sign."""

def test_gcp(self):
"""Test that GCP KMS key works for signing
In case of problems with KMS account, please file an issue and
assign @jku
"""

data = "data".encode("utf-8")

pubkey = {
"keyid": "abcd",
"keytype": "ecdsa",
"scheme": "ecdsa-sha2-nistp256",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/ptvrXYuUc2ZaKssHhtg/IKNbO1X\ncDWlbKqLNpaK62MKdOwDz1qlp5AGHZkTY9tO09iq1F16SvVot1BQ9FJ2dw==\n-----END PUBLIC KEY-----\n"
},
}

gcp_id = "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1"
hash_algo = "sha256"

signer = GCPSigner(gcp_id, hash_algo, pubkey["keyid"])
sig = signer.sign(data)

self.AssertTrue(keys.verify_signature(pubkey, sig.to_dict(), data))


if __name__ == "__main__":
unittest.main(verbosity=1, buffer=True)
7 changes: 7 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ setenv =
commands =
python -m tests.check_public_interfaces_gpg

[testenv:kms]
deps =
-r{toxinidir}/requirements-pinned.txt
-r{toxinidir}/requirements-kms.txt
commands =
python -m tests.check_kms_signers

# This checks that importing securesystemslib.gpg.constants doesn't shell out on
# import.
[testenv:py38-test-gpg-fails]
Expand Down

0 comments on commit 07ee954

Please sign in to comment.