Skip to content

Commit 86b44c0

Browse files
authored
Merge pull request #480 from jku/gcp-import
GCPSigner import
2 parents 75cfb45 + f8c851f commit 86b44c0

File tree

5 files changed

+138
-40
lines changed

5 files changed

+138
-40
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Issues = "https://github.com/secure-systems-lab/securesystemslib/issues"
4444

4545
[project.optional-dependencies]
4646
crypto = ["cryptography>=37.0.0"]
47-
gcpkms = ["google-cloud-kms"]
47+
gcpkms = ["google-cloud-kms", "cryptography>=37.0.0"]
4848
pynacl = ["pynacl>1.2.0"]
4949
PySPX = ["PySPX==0.5.0"]
5050
asn1 = ["asn1crypto"]

securesystemslib/signer/_gcp_signer.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
"""Signer implementation for Google Cloud KMS"""
22

33
import logging
4-
from typing import Optional
4+
from typing import Optional, Tuple
55
from urllib import parse
66

77
import securesystemslib.hash as sslib_hash
88
from securesystemslib import exceptions
9+
from securesystemslib.keys import _get_keyid
910
from securesystemslib.signer._key import Key
10-
from securesystemslib.signer._signer import SecretsHandler, Signature, Signer
11+
from securesystemslib.signer._signer import (
12+
SecretsHandler,
13+
Signature,
14+
Signer,
15+
SSlibKey,
16+
)
1117

1218
logger = logging.getLogger(__name__)
1319

1420
GCP_IMPORT_ERROR = None
1521
try:
1622
from google.cloud import kms
23+
from google.cloud.kms_v1.types import CryptoKeyVersion
1724
except ImportError:
1825
GCP_IMPORT_ERROR = (
1926
"google-cloud-kms library required to sign with Google Cloud keys."
@@ -29,9 +36,14 @@ class GCPSigner(Signer):
2936
The signer uses "ambient" credentials: typically environment var
3037
GOOGLE_APPLICATION_CREDENTIALS that points to a file with valid
3138
credentials. These will be found by google.cloud.kms, see
32-
https://cloud.google.com/docs/authentication/getting-started
33-
(and https://github.com/google-github-actions/auth for the relevant
34-
GitHub action).
39+
https://cloud.google.com/docs/authentication/getting-started.
40+
Some practical authentication options include:
41+
* GitHub Action: https://github.com/google-github-actions/auth
42+
* gcloud CLI: https://cloud.google.com/sdk/gcloud
43+
44+
The specific permissions that GCPSigner needs are:
45+
* roles/cloudkms.signer for sign()
46+
* roles/cloudkms.publicKeyViewer for import()
3547
3648
Arguments:
3749
gcp_keyid: Fully qualified GCP KMS key name, like
@@ -71,6 +83,79 @@ def from_priv_key_uri(
7183

7284
return cls(uri.path, public_key)
7385

86+
@classmethod
87+
def import_(cls, gcp_keyid: str) -> Tuple[str, Key]:
88+
"""Load key and signer details from KMS
89+
90+
Returns the private key uri and the public key. This method should only
91+
be called once per key: the uri and Key should be stored for later use.
92+
"""
93+
if GCP_IMPORT_ERROR:
94+
raise exceptions.UnsupportedLibraryError(GCP_IMPORT_ERROR)
95+
96+
client = kms.KeyManagementServiceClient()
97+
request = {"name": gcp_keyid}
98+
kms_pubkey = client.get_public_key(request)
99+
try:
100+
keytype, scheme = cls._get_keytype_and_scheme(kms_pubkey.algorithm)
101+
except KeyError as e:
102+
raise exceptions.UnsupportedAlgorithmError(
103+
f"{kms_pubkey.algorithm} is not a supported signing algorithm"
104+
) from e
105+
106+
keyval = {"public": kms_pubkey.pem}
107+
keyid = _get_keyid(keytype, scheme, keyval)
108+
public_key = SSlibKey(keyid, keytype, scheme, keyval)
109+
110+
return f"{cls.SCHEME}:{gcp_keyid}", public_key
111+
112+
@staticmethod
113+
def _get_keytype_and_scheme(algorithm: int) -> Tuple[str, str]:
114+
"""Return keytype and scheme for the KMS algorithm enum"""
115+
keytypes_and_schemes = {
116+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.EC_SIGN_P256_SHA256: (
117+
"ecdsa",
118+
"ecdsa-sha2-nistp256",
119+
),
120+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.EC_SIGN_P384_SHA384: (
121+
"ecdsa",
122+
"ecdsa-sha2-nistp384",
123+
),
124+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_2048_SHA256: (
125+
"rsa",
126+
"rsassa-pss-sha256",
127+
),
128+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_3072_SHA256: (
129+
"rsa",
130+
"rsassa-pss-sha256",
131+
),
132+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_4096_SHA256: (
133+
"rsa",
134+
"rsassa-pss-sha256",
135+
),
136+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_4096_SHA512: (
137+
"rsa",
138+
"rsassa-pss-sha512",
139+
),
140+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_2048_SHA256: (
141+
"rsa",
142+
"rsa-pkcs1v15-sha256",
143+
),
144+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_3072_SHA256: (
145+
"rsa",
146+
"rsa-pkcs1v15-sha256",
147+
),
148+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_4096_SHA256: (
149+
"rsa",
150+
"rsa-pkcs1v15-sha256",
151+
),
152+
CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_4096_SHA512: (
153+
"rsa",
154+
"rsa-pkcs1v15-sha512",
155+
),
156+
}
157+
return keytypes_and_schemes[algorithm]
158+
74159
@staticmethod
75160
def _get_hash_algorithm(public_key: Key) -> str:
76161
"""Helper function to return payload hash algorithm used for this key"""

securesystemslib/signer/_hsm_signer.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from securesystemslib import KEY_TYPE_ECDSA
1313
from securesystemslib.exceptions import UnsupportedLibraryError
14+
from securesystemslib.keys import _get_keyid
1415
from securesystemslib.signer._key import Key, SSlibKey
1516
from securesystemslib.signer._signature import Signature
1617
from securesystemslib.signer._signer import SecretsHandler, Signer
@@ -191,13 +192,14 @@ def _find_key_values(
191192
return ECDomainParameters.load(bytes(params)), bytes(point)
192193

193194
@classmethod
194-
def pubkey_from_hsm(
195-
cls, sslib_keyid: str, hsm_keyid: Optional[int] = None
196-
) -> SSlibKey:
197-
"""Export public key from HSM.
195+
def import_(cls, hsm_keyid: Optional[int] = None) -> Tuple[str, SSlibKey]:
196+
"""Import public key and signer details from HSM.
197+
198+
Returns a private key URI (for Signer.from_priv_key_uri()) and a public
199+
key. import_() should be called once and the returned URI and public
200+
key should be stored for later use.
198201
199202
Arguments:
200-
sslib_keyid: Key identifier that is unique within the metadata it is used in.
201203
hsm_keyid: Key identifier on the token. Default is 2 (meaning PIV key slot 9c).
202204
203205
Raises:
@@ -240,12 +242,12 @@ def pubkey_from_hsm(
240242
.decode()
241243
)
242244

243-
return SSlibKey(
244-
sslib_keyid,
245-
KEY_TYPE_ECDSA,
246-
_SCHEME_FOR_CURVE[curve],
247-
{"public": public_pem},
248-
)
245+
keyval = {"public": public_pem}
246+
scheme = _SCHEME_FOR_CURVE[curve]
247+
keyid = _get_keyid(KEY_TYPE_ECDSA, scheme, keyval)
248+
key = SSlibKey(keyid, KEY_TYPE_ECDSA, scheme, keyval)
249+
250+
return "hsm:", key
249251

250252
@classmethod
251253
def from_priv_key_uri(

tests/check_kms_signers.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,25 @@
1818
import unittest
1919

2020
from securesystemslib.exceptions import UnverifiedSignatureError
21-
from securesystemslib.signer import Key, Signer
21+
from securesystemslib.signer import GCPSigner, Key, Signer
2222

2323

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

27-
def test_gcp(self):
27+
pubkey = Key.from_dict(
28+
"218611b80052667026c221f8774249b0f6b8b310d30a5c45a3b878aa3a02f39e",
29+
{
30+
"keytype": "ecdsa",
31+
"scheme": "ecdsa-sha2-nistp256",
32+
"keyval": {
33+
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/ptvrXYuUc2ZaKssHhtg/IKNbO1X\ncDWlbKqLNpaK62MKdOwDz1qlp5AGHZkTY9tO09iq1F16SvVot1BQ9FJ2dw==\n-----END PUBLIC KEY-----\n"
34+
},
35+
},
36+
)
37+
gcp_id = "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1"
38+
39+
def test_gcp_sign(self):
2840
"""Test that GCP KMS key works for signing
2941
3042
NOTE: The KMS account is setup to only accept requests from the
@@ -35,25 +47,27 @@ def test_gcp(self):
3547
"""
3648

3749
data = "data".encode("utf-8")
38-
pubkey = Key.from_dict(
39-
"abcd",
40-
{
41-
"keyid": "abcd",
42-
"keytype": "ecdsa",
43-
"scheme": "ecdsa-sha2-nistp256",
44-
"keyval": {
45-
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/ptvrXYuUc2ZaKssHhtg/IKNbO1X\ncDWlbKqLNpaK62MKdOwDz1qlp5AGHZkTY9tO09iq1F16SvVot1BQ9FJ2dw==\n-----END PUBLIC KEY-----\n"
46-
},
47-
},
48-
)
49-
gcp_id = "projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1"
5050

51-
signer = Signer.from_priv_key_uri(f"gcpkms:{gcp_id}", pubkey)
51+
signer = Signer.from_priv_key_uri(f"gcpkms:{self.gcp_id}", self.pubkey)
5252
sig = signer.sign(data)
5353

54-
pubkey.verify_signature(sig, data)
54+
self.pubkey.verify_signature(sig, data)
5555
with self.assertRaises(UnverifiedSignatureError):
56-
pubkey.verify_signature(sig, b"NOT DATA")
56+
self.pubkey.verify_signature(sig, b"NOT DATA")
57+
58+
def test_gcp_import(self):
59+
"""Test that GCP KMS key can be imported
60+
61+
NOTE: The KMS account is setup to only accept requests from the
62+
Securesystemslib GitHub Action environment: test cannot pass elsewhere.
63+
64+
In case of problems with KMS account, please file an issue and
65+
assign @jku.
66+
"""
67+
68+
uri, key = GCPSigner.import_(self.gcp_id)
69+
self.assertEqual(key, self.pubkey)
70+
self.assertEqual(uri, f"gcpkms:{self.gcp_id}")
5771

5872

5973
if __name__ == "__main__":

tests/test_hsm_signer.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ class TestHSM(unittest.TestCase):
5151
See .github/workflows/hsm.yml for how this can be done on Linux, macOS and Windows.
5252
"""
5353

54-
sslib_keyid = "a" * 64 # Mock SSlibKey conform sha256 hex digest keyid
5554
hsm_keyid = 1
5655
hsm_keyid_default = 2
5756
hsm_user_pin = "123456"
@@ -139,7 +138,7 @@ def test_hsm(self):
139138
"""Test HSM key export and signing."""
140139

141140
for hsm_keyid in [self.hsm_keyid, self.hsm_keyid_default]:
142-
key = HSMSigner.pubkey_from_hsm(self.sslib_keyid, hsm_keyid)
141+
_, key = HSMSigner.import_(hsm_keyid)
143142
signer = HSMSigner(hsm_keyid, key, lambda sec: self.hsm_user_pin)
144143
sig = signer.sign(b"DATA")
145144
key.verify_signature(sig, b"DATA")
@@ -150,11 +149,9 @@ def test_hsm(self):
150149
def test_hsm_uri(self):
151150
"""Test HSM default key export and signing from URI."""
152151

153-
key = HSMSigner.pubkey_from_hsm(
154-
self.sslib_keyid, self.hsm_keyid_default
155-
)
152+
uri, key = HSMSigner.import_(self.hsm_keyid_default)
156153
signer = Signer.from_priv_key_uri(
157-
"hsm:", key, lambda sec: self.hsm_user_pin
154+
uri, key, lambda sec: self.hsm_user_pin
158155
)
159156
sig = signer.sign(b"DATA")
160157
key.verify_signature(sig, b"DATA")

0 commit comments

Comments
 (0)