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-pinned.txt...
  Not sure what the best path here is
* Test only works on GitHub (because of the authentication),
  and only on branches within the upstream repo: not on PRs from forks
* Test is run only once: it's a smoke test, not an exhaustive matrix
  test.
  • Loading branch information
jku committed Nov 4, 2022
1 parent 4e63f99 commit b0033c7
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/test-kms.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Run KMS tests

on:
push:
workflow_dispatch:

permissions: {}

jobs:
test-kms:
runs-on: ubuntu-latest

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

steps:
- name: Checkout securesystemslib
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
cache: 'pip'
cache-dependency-path: 'requirements*.txt'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade tox
- name: Authenticate to Google Cloud
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

- run: tox -e kms
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
79 changes: 79 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,69 @@ 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:
UnsupportedAlgorithmError: The payload hash algorithm is unsupported.
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.hash_algo = hash_algo
# trigger exception if algorithm is unsupported
_ = sslib_hash.digest(self.hash_algo)

self.gcp_keyid = gcp_keyid
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:
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 @@ -101,6 +101,7 @@
python_requires="~=3.7",
extras_require={
"crypto": ["cryptography>=37.0.0"],
"gcpkms": ["google-cloud-kms"],
"pynacl": ["pynacl>1.2.0"],
"PySPX": ["PySPX==0.5.0"],
},
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 b0033c7

Please sign in to comment.