Skip to content

Commit 4831bba

Browse files
added kms asymmetric samples (GoogleCloudPlatform#1638)
1 parent 603bfc0 commit 4831bba

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

kms/api-client/asymmetric.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/bin/python
2+
# Copyright 2018 Google Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.rom googleapiclient import discovery
15+
16+
import base64
17+
import hashlib
18+
19+
from cryptography.exceptions import InvalidSignature
20+
from cryptography.hazmat.backends import default_backend
21+
from cryptography.hazmat.primitives import hashes, serialization
22+
from cryptography.hazmat.primitives.asymmetric import ec, padding, utils
23+
24+
25+
# [START kms_get_asymmetric_public]
26+
def getAsymmetricPublicKey(client, key_path):
27+
"""Retrieves the public key from a saved asymmetric key pair on Cloud KMS
28+
"""
29+
request = client.projects() \
30+
.locations() \
31+
.keyRings() \
32+
.cryptoKeys() \
33+
.cryptoKeyVersions() \
34+
.getPublicKey(name=key_path)
35+
response = request.execute()
36+
key_txt = response['pem'].encode('ascii')
37+
key = serialization.load_pem_public_key(key_txt, default_backend())
38+
return key
39+
# [END kms_get_asymmetric_public]
40+
41+
42+
# [START kms_decrypt_rsa]
43+
def decryptRSA(ciphertext, client, key_path):
44+
"""Decrypt a given ciphertext using an RSA private key stored on Cloud KMS
45+
"""
46+
request = client.projects() \
47+
.locations() \
48+
.keyRings() \
49+
.cryptoKeys() \
50+
.cryptoKeyVersions() \
51+
.asymmetricDecrypt(name=key_path,
52+
body={'ciphertext': ciphertext})
53+
response = request.execute()
54+
plaintext = base64.b64decode(response['plaintext']).decode('utf-8')
55+
return plaintext
56+
# [END kms_decrypt_rsa]
57+
58+
59+
# [START kms_encrypt_rsa]
60+
def encryptRSA(message, client, key_path):
61+
"""Encrypt message locally using an RSA public key retrieved from Cloud KMS
62+
"""
63+
public_key = getAsymmetricPublicKey(client, key_path)
64+
pad = padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
65+
algorithm=hashes.SHA256(),
66+
label=None)
67+
ciphertext = public_key.encrypt(message.encode('ascii'), pad)
68+
ciphertext = base64.b64encode(ciphertext).decode('utf-8')
69+
return ciphertext
70+
# [END kms_encrypt_rsa]
71+
72+
73+
# [START kms_sign_asymmetric]
74+
def signAsymmetric(message, client, key_path):
75+
"""Create a signature for a message using a private key stored on Cloud KMS
76+
"""
77+
digest_bytes = hashlib.sha256(message.encode('ascii')).digest()
78+
digest64 = base64.b64encode(digest_bytes)
79+
80+
digest_JSON = {'sha256': digest64.decode('utf-8')}
81+
request = client.projects() \
82+
.locations() \
83+
.keyRings() \
84+
.cryptoKeys() \
85+
.cryptoKeyVersions() \
86+
.asymmetricSign(name=key_path,
87+
body={'digest': digest_JSON})
88+
response = request.execute()
89+
return response.get('signature', None)
90+
# [END kms_sign_asymmetric]
91+
92+
93+
# [START kms_verify_signature_rsa]
94+
def verifySignatureRSA(signature, message, client, key_path):
95+
"""Verify the validity of an 'RSA_SIGN_PSS_2048_SHA256' signature
96+
for the specified plaintext message
97+
"""
98+
public_key = getAsymmetricPublicKey(client, key_path)
99+
100+
digest_bytes = hashlib.sha256(message.encode('ascii')).digest()
101+
sig_bytes = base64.b64decode(signature)
102+
103+
try:
104+
# Attempt verification
105+
public_key.verify(sig_bytes,
106+
digest_bytes,
107+
padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
108+
salt_length=32),
109+
utils.Prehashed(hashes.SHA256()))
110+
# No errors were thrown. Verification was successful
111+
return True
112+
except InvalidSignature:
113+
return False
114+
# [END kms_verify_signature_rsa]
115+
116+
117+
# [START kms_verify_signature_ec]
118+
def verifySignatureEC(signature, message, client, key_path):
119+
"""Verify the validity of an 'EC_SIGN_P224_SHA256' signature
120+
for the specified plaintext message
121+
"""
122+
public_key = getAsymmetricPublicKey(client, key_path)
123+
124+
digest_bytes = hashlib.sha256(message.encode('ascii')).digest()
125+
sig_bytes = base64.b64decode(signature)
126+
127+
try:
128+
# Attempt verification
129+
public_key.verify(sig_bytes,
130+
digest_bytes,
131+
ec.ECDSA(utils.Prehashed(hashes.SHA256())))
132+
# No errors were thrown. Verification was successful
133+
return True
134+
except InvalidSignature:
135+
return False
136+
# [END kms_verify_signature_ec]

kms/api-client/asymmetric_test.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/bin/python
2+
# Copyright 2018 Google Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from os import environ
18+
from time import sleep
19+
20+
from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePublicKey
21+
from cryptography.hazmat.backends.openssl.rsa import _RSAPublicKey
22+
from googleapiclient import discovery
23+
from googleapiclient.errors import HttpError
24+
import sample
25+
26+
27+
def create_key_helper(key_id, key_path, purpose, algorithm, t):
28+
try:
29+
t.client.projects() \
30+
.locations() \
31+
.keyRings() \
32+
.cryptoKeys() \
33+
.create(parent='{}/keyRings/{}'.format(t.parent, t.keyring),
34+
body={'purpose': purpose,
35+
'versionTemplate': {
36+
'algorithm': algorithm
37+
}
38+
},
39+
cryptoKeyId=key_id) \
40+
.execute()
41+
return True
42+
except HttpError:
43+
# key already exists
44+
return False
45+
46+
47+
def setup_module(module):
48+
"""
49+
Set up keys in project if needed
50+
"""
51+
t = TestKMSSamples()
52+
try:
53+
# create keyring
54+
t.client.projects() \
55+
.locations() \
56+
.keyRings() \
57+
.create(parent=t.parent, body={}, keyRingId=t.keyring) \
58+
.execute()
59+
except HttpError:
60+
# keyring already exists
61+
pass
62+
s1 = create_key_helper(t.rsaDecryptId, t.rsaDecrypt, 'ASYMMETRIC_DECRYPT',
63+
'RSA_DECRYPT_OAEP_2048_SHA256', t)
64+
s2 = create_key_helper(t.rsaSignId, t.rsaSign, 'ASYMMETRIC_SIGN',
65+
'RSA_SIGN_PSS_2048_SHA256', t)
66+
s3 = create_key_helper(t.ecSignId, t.ecSign, 'ASYMMETRIC_SIGN',
67+
'EC_SIGN_P224_SHA256', t)
68+
if s1 or s2 or s3:
69+
# leave time for keys to initialize
70+
sleep(20)
71+
72+
73+
class TestKMSSamples:
74+
75+
project_id = environ['GCLOUD_PROJECT']
76+
keyring = 'kms-asymmetric-samples4'
77+
parent = 'projects/{}/locations/global'.format(project_id)
78+
79+
rsaSignId = 'rsa-sign'
80+
rsaDecryptId = 'rsa-decrypt'
81+
ecSignId = 'ec-sign'
82+
83+
rsaSign = '{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/1' \
84+
.format(parent, keyring, rsaSignId)
85+
rsaDecrypt = '{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/1' \
86+
.format(parent, keyring, rsaDecryptId)
87+
ecSign = '{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/1' \
88+
.format(parent, keyring, ecSignId)
89+
90+
message = 'test message 123'
91+
92+
client = discovery.build('cloudkms', 'v1')
93+
94+
def test_get_public_key(self):
95+
rsa_key = sample.getAsymmetricPublicKey(self.client, self.rsaDecrypt)
96+
assert isinstance(rsa_key, _RSAPublicKey), 'expected RSA key'
97+
ec_key = sample.getAsymmetricPublicKey(self.client, self.ecSign)
98+
assert isinstance(ec_key, _EllipticCurvePublicKey), 'expected EC key'
99+
100+
def test_rsa_encrypt_decrypt(self):
101+
ciphertext = sample.encryptRSA(self.message,
102+
self.client,
103+
self.rsaDecrypt)
104+
# ciphertext should be 344 characters with base64 and RSA 2048
105+
assert len(ciphertext) == 344, \
106+
'ciphertext should be 344 chars; got {}'.format(len(ciphertext))
107+
assert ciphertext[-2:] == '==', 'cipher text should end with =='
108+
plaintext = sample.decryptRSA(ciphertext, self.client, self.rsaDecrypt)
109+
assert plaintext == self.message
110+
111+
def test_rsa_sign_verify(self):
112+
sig = sample.signAsymmetric(self.message, self.client, self.rsaSign)
113+
# ciphertext should be 344 characters with base64 and RSA 2048
114+
assert len(sig) == 344, \
115+
'sig should be 344 chars; got {}'.format(len(sig))
116+
assert sig[-2:] == '==', 'sig should end with =='
117+
success = sample.verifySignatureRSA(sig,
118+
self.message,
119+
self.client,
120+
self.rsaSign)
121+
assert success is True, 'RSA verification failed'
122+
success = sample.verifySignatureRSA(sig,
123+
self.message+'.',
124+
self.client,
125+
self.rsaSign)
126+
assert success is False, 'verify should fail with modified message'
127+
128+
def test_ec_sign_verify(self):
129+
sig = sample.signAsymmetric(self.message, self.client, self.ecSign)
130+
assert len(sig) > 50 and len(sig) < 300, \
131+
'sig outside expected length range'
132+
success = sample.verifySignatureEC(sig,
133+
self.message,
134+
self.client,
135+
self.ecSign)
136+
assert success is True, 'EC verification failed'
137+
success = sample.verifySignatureEC(sig,
138+
self.message+'.',
139+
self.client,
140+
self.ecSign)
141+
assert success is False, 'verify should fail with modified message'

kms/api-client/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
google-api-python-client==1.6.6
22
google-auth==1.4.1
33
google-auth-httplib2==0.0.3
4+
cryptography==2.3.1

0 commit comments

Comments
 (0)