Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/signer/_gcp_signer.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

63 statements  

1"""Signer implementation for Google Cloud KMS""" 

2 

3from __future__ import annotations 

4 

5import hashlib 

6import logging 

7from urllib import parse 

8 

9from securesystemslib import exceptions 

10from securesystemslib.signer._key import Key, SSlibKey 

11from securesystemslib.signer._signer import SecretsHandler, Signature, Signer 

12from securesystemslib.signer._utils import compute_default_keyid 

13 

14logger = logging.getLogger(__name__) 

15 

16GCP_IMPORT_ERROR = None 

17try: 

18 from google.cloud import kms 

19 from google.cloud.kms_v1.types import CryptoKeyVersion 

20 

21 KEYTYPES_AND_SCHEMES = { 

22 CryptoKeyVersion.CryptoKeyVersionAlgorithm.EC_SIGN_P256_SHA256: ( 

23 "ecdsa", 

24 "ecdsa-sha2-nistp256", 

25 ), 

26 CryptoKeyVersion.CryptoKeyVersionAlgorithm.EC_SIGN_P384_SHA384: ( 

27 "ecdsa", 

28 "ecdsa-sha2-nistp384", 

29 ), 

30 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_2048_SHA256: ( 

31 "rsa", 

32 "rsassa-pss-sha256", 

33 ), 

34 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_3072_SHA256: ( 

35 "rsa", 

36 "rsassa-pss-sha256", 

37 ), 

38 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_4096_SHA256: ( 

39 "rsa", 

40 "rsassa-pss-sha256", 

41 ), 

42 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PSS_4096_SHA512: ( 

43 "rsa", 

44 "rsassa-pss-sha512", 

45 ), 

46 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_2048_SHA256: ( 

47 "rsa", 

48 "rsa-pkcs1v15-sha256", 

49 ), 

50 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_3072_SHA256: ( 

51 "rsa", 

52 "rsa-pkcs1v15-sha256", 

53 ), 

54 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_4096_SHA256: ( 

55 "rsa", 

56 "rsa-pkcs1v15-sha256", 

57 ), 

58 CryptoKeyVersion.CryptoKeyVersionAlgorithm.RSA_SIGN_PKCS1_4096_SHA512: ( 

59 "rsa", 

60 "rsa-pkcs1v15-sha512", 

61 ), 

62 } 

63except ImportError: 

64 GCP_IMPORT_ERROR = ( 

65 "google-cloud-kms library required to sign with Google Cloud keys." 

66 ) 

67 

68 

69class GCPSigner(Signer): 

70 """Google Cloud KMS Signer 

71 

72 This Signer uses Google Cloud KMS to sign: the payload is hashed locally, 

73 but the signature is created on the KMS. 

74 

75 The signer uses "ambient" credentials: typically environment var 

76 GOOGLE_APPLICATION_CREDENTIALS that points to a file with valid 

77 credentials. These will be found by google.cloud.kms, see 

78 https://cloud.google.com/docs/authentication/getting-started. 

79 Some practical authentication options include: 

80 * GitHub Action: https://github.com/google-github-actions/auth 

81 * gcloud CLI: https://cloud.google.com/sdk/gcloud 

82 

83 The specific permissions that GCPSigner needs are: 

84 * roles/cloudkms.signer for sign() 

85 * roles/cloudkms.publicKeyViewer for import() 

86 

87 Arguments: 

88 gcp_keyid: Fully qualified GCP KMS key name, like 

89 projects/python-tuf-kms/locations/global/keyRings/securesystemslib-tests/cryptoKeys/ecdsa-sha2-nistp256/cryptoKeyVersions/1 

90 public_key: The related public key instance 

91 

92 Raises: 

93 UnsupportedAlgorithmError: The payload hash algorithm is unsupported. 

94 UnsupportedLibraryError: google.cloud.kms was not found 

95 Various errors from google.cloud modules: e.g. 

96 google.auth.exceptions.DefaultCredentialsError if ambient 

97 credentials are not found 

98 """ 

99 

100 SCHEME = "gcpkms" 

101 

102 def __init__(self, gcp_keyid: str, public_key: SSlibKey): 

103 if GCP_IMPORT_ERROR: 

104 raise exceptions.UnsupportedLibraryError(GCP_IMPORT_ERROR) 

105 

106 if (public_key.keytype, public_key.scheme) not in KEYTYPES_AND_SCHEMES.values(): 

107 raise exceptions.UnsupportedAlgorithmError( 

108 f"Unsupported key ({public_key.keytype}/{public_key.scheme}) " 

109 f"in key {public_key.keyid}" 

110 ) 

111 

112 self.hash_algorithm = public_key.get_hash_algorithm_name() 

113 self.gcp_keyid = gcp_keyid 

114 self._public_key = public_key 

115 self.client = kms.KeyManagementServiceClient() 

116 

117 @property 

118 def public_key(self) -> SSlibKey: 

119 return self._public_key 

120 

121 @classmethod 

122 def from_priv_key_uri( 

123 cls, 

124 priv_key_uri: str, 

125 public_key: Key, 

126 secrets_handler: SecretsHandler | None = None, 

127 ) -> GCPSigner: 

128 if not isinstance(public_key, SSlibKey): 

129 raise ValueError(f"Expected SSlibKey for {priv_key_uri}") 

130 

131 uri = parse.urlparse(priv_key_uri) 

132 

133 if uri.scheme != cls.SCHEME: 

134 raise ValueError(f"GCPSigner does not support {priv_key_uri}") 

135 

136 return cls(uri.path, public_key) 

137 

138 @classmethod 

139 def import_(cls, gcp_keyid: str) -> tuple[str, SSlibKey]: 

140 """Load key and signer details from KMS 

141 

142 Returns the private key uri and the public key. This method should only 

143 be called once per key: the uri and Key should be stored for later use. 

144 """ 

145 if GCP_IMPORT_ERROR: 

146 raise exceptions.UnsupportedLibraryError(GCP_IMPORT_ERROR) 

147 

148 client = kms.KeyManagementServiceClient() 

149 request = {"name": gcp_keyid} 

150 kms_pubkey = client.get_public_key(request) 

151 try: 

152 keytype, scheme = KEYTYPES_AND_SCHEMES[kms_pubkey.algorithm] 

153 except KeyError as e: 

154 raise exceptions.UnsupportedAlgorithmError( 

155 f"{kms_pubkey.algorithm} is not a supported signing algorithm" 

156 ) from e 

157 

158 keyval = {"public": kms_pubkey.pem} 

159 keyid = compute_default_keyid(keytype, scheme, keyval) 

160 public_key = SSlibKey(keyid, keytype, scheme, keyval) 

161 

162 return f"{cls.SCHEME}:{gcp_keyid}", public_key 

163 

164 def sign(self, payload: bytes) -> Signature: 

165 """Signs payload with Google Cloud KMS. 

166 

167 Arguments: 

168 payload: bytes to be signed. 

169 

170 Raises: 

171 Various errors from google.cloud modules. 

172 

173 Returns: 

174 Signature. 

175 """ 

176 # NOTE: request and response can contain CRC32C of the digest/sig: 

177 # Verifying could be useful but would require another dependency... 

178 

179 hasher = hashlib.new(self.hash_algorithm) 

180 hasher.update(payload) 

181 digest = {self.hash_algorithm: hasher.digest()} 

182 request = {"name": self.gcp_keyid, "digest": digest} 

183 

184 logger.debug("signing request %s", request) 

185 response = self.client.asymmetric_sign(request) 

186 logger.debug("signing response %s", response) 

187 

188 return Signature(self.public_key.keyid, response.signature.hex())