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 payload hashing happens in Signer).

This is not super usable without issue 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 3, 2022
1 parent 0b71aaa commit ba9ffbf
Show file tree
Hide file tree
Showing 6 changed files with 161 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: 'securesystemslib-tests@python-tuf-kms.iam.gserviceaccount.com'

- 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
77 changes: 77 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,67 @@ 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
This Signer uses Google Cloud KMS to sign: the payload is hashed locally,
but the signature is created on the KMS.
The signer uses "ambient" credentials: typically environment var
GOOGLE_APPLICATION_CREDENTIALS that points to a file with valid
credentials. These will be found by google.cloud.kms, see
https://cloud.google.com/docs/authentication/getting-started
(and https://github.com/google-github-actions/auth for the relevant
GitHub action).
Arguments:
gcp_keyid: Fully qualified GCP KMS key name, like
projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1
hash_algo: Payload hashing algorithm
keyid: The keyid to be used in the returned Signature
Raises:
UnsupportedLibraryError: google.cloud.kms was not found
Various errors from google.cloud modules: e.g.
google.auth.exceptions.DefaultCredentialsError if ambient
credentials are not found
"""

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:
"""Signs payload with Google Cloud KMS.
Arguments:
payload: bytes to be signed.
Raises:
FormatError, UnsupportedAlgorithmError:
The hash algorithm is unusable.
Various errors from google.cloud modules.
Returns:
Signature.
"""
# NOTE: request and response can contain CRC32C of the digest/sig:
# Verifying 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
58 changes: 58 additions & 0 deletions tests/check_kms_signers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python

"""
This module confirms that signing using KMS keys works.
The purpose is to do a smoke test, not to exhaustively test every possible
key and environment combination.
For Google Cloud (GCP), the requirements to successfully test are:
* Google Cloud authentication details have to be available in the environment
* The key defined in the test has to be available to the authenticated user
NOTE: the filename is purposefully check_ rather than test_ so that tests are
only run when explicitly invoked: The tests can only pass on Securesystemslib
GitHub Action environment because of the above requirements.
"""

import unittest

from securesystemslib import keys
from securesystemslib.signer import GCPSigner


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
NOTE: The KMS account is setup to only accept requests from the
Securesystemslib GitHub Action environment: test cannot pass elsewhere.
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)
9 changes: 9 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ setenv =
commands =
python -m tests.check_public_interfaces_gpg

[testenv:kms]
deps =
-r{toxinidir}/requirements-pinned.txt
-r{toxinidir}/requirements-kms.txt
passenv =
GOOGLE_APPLICATION_CREDENTIALS
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 ba9ffbf

Please sign in to comment.