Skip to content

Commit 62862fc

Browse files
bkuangBenson Kuang
authored andcommitted
feat: add script to verify attestations with certificate chains (#99)
Co-authored-by: Benson Kuang <[email protected]>
1 parent 6dbc822 commit 62862fc

File tree

2 files changed

+617
-0
lines changed

2 files changed

+617
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2021 Google, LLC.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
"""This application verifies HSM certificate chains.
17+
18+
For more information, visit https://cloud.google.com/kms/docs/attest-key.
19+
"""
20+
21+
# [START kms_verify_chains]
22+
import argparse
23+
import gzip
24+
import io
25+
import zipfile
26+
27+
from cryptography import exceptions
28+
from cryptography import x509
29+
from cryptography.hazmat import backends
30+
from cryptography.hazmat.primitives import hashes
31+
from cryptography.hazmat.primitives import serialization
32+
from cryptography.hazmat.primitives.asymmetric import padding
33+
import pem
34+
import requests
35+
36+
ATTESTATION_SIGNATURE_LEN = 256
37+
38+
MANUFACTURER_CERT_URL = 'https://www.marvell.com/content/dam/marvell/en/public-collateral/security-solutions/liquid_security_certificate.zip'
39+
40+
# <Name(C=US,ST=California,L=San Jose,O=Cavium\, Inc.,OU=LiquidSecurity,CN=localca.liquidsecurity.cavium.com)>
41+
MANUFACTURER_CERT_SUBJECT_BYTES = (
42+
b'0\x81\x911\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\x08'
43+
b'\x0c\nCalifornia1\x110\x0f\x06\x03U\x04\x07\x0c\x08San Jose1\x150\x13\x06'
44+
b'\x03U\x04\n\x0c\x0cCavium, Inc.1\x170\x15\x06\x03U\x04\x0b\x0c\x0e'
45+
b'LiquidSecurity1*0(\x06\x03U\x04\x03\x0c!localca.liquidsecurity.cavium.com'
46+
)
47+
48+
# The owner root cert can be obtained from
49+
# 'https://www.gstatic.com/cloudhsm/roots/global_1498867200.pem'
50+
OWNER_ROOT_CERT_PEM = """Certificate:
51+
Data:
52+
Version: 3 (0x2)
53+
Serial Number: 3 (0x3)
54+
Signature Algorithm: sha256WithRSAEncryption
55+
Issuer: C = US, ST = CA, L = Mountain View, O = Google Inc, CN = Hawksbill Root v1 prod
56+
Validity
57+
Not Before: Jul 1 00:00:00 2017 GMT
58+
Not After : Jan 1 00:00:00 2030 GMT
59+
Subject: C = US, ST = CA, L = Mountain View, O = Google Inc, CN = Hawksbill Root v1 prod
60+
Subject Public Key Info:
61+
Public Key Algorithm: rsaEncryption
62+
RSA Public-Key: (2048 bit)
63+
Modulus:
64+
00:ac:2e:a8:62:89:21:a0:70:92:df:b0:8e:c3:93:
65+
4d:26:38:db:a5:a2:5f:6b:1e:6d:a8:6c:2c:83:d6:
66+
5b:9a:f8:02:a0:f8:b0:16:fb:5c:da:b9:9b:b9:8b:
67+
4d:bc:15:26:e0:0e:4f:2f:b5:20:43:1c:31:7e:5e:
68+
c1:67:a9:36:c8:19:5e:c2:b5:a8:b6:96:76:90:7b:
69+
55:15:4d:53:16:10:f0:62:d5:d8:98:19:c7:9e:0e:
70+
b2:69:26:a3:f3:d9:a5:d3:70:88:21:ac:62:12:7b:
71+
2a:be:20:2e:33:db:9b:90:a7:b1:bf:0f:c0:11:7a:
72+
c2:98:a9:8c:4d:36:a7:1f:66:53:08:93:4b:3a:12:
73+
1e:1a:3f:2b:c2:5d:8b:4b:97:d4:17:0f:41:83:27:
74+
a9:f3:e0:d9:82:f8:5c:37:d4:1e:5d:e4:a8:3d:59:
75+
7c:43:64:e6:02:d7:35:39:f4:95:db:77:1c:73:78:
76+
2f:c4:26:8d:64:d4:01:e0:86:da:3f:27:c7:9d:bd:
77+
32:25:e4:d4:34:6a:13:87:2a:85:19:ce:18:43:46:
78+
c5:41:8a:81:66:ca:65:6e:c1:a1:ce:71:74:d4:b0:
79+
77:b7:35:39:0d:c9:e2:c8:7e:81:69:b1:04:38:5d:
80+
c1:fd:92:33:ba:ed:85:d3:91:d0:96:78:d6:30:fc:
81+
56:19
82+
Exponent: 65537 (0x10001)
83+
X509v3 extensions:
84+
X509v3 Basic Constraints: critical
85+
CA:TRUE
86+
X509v3 Key Usage: critical
87+
Digital Signature, Certificate Sign, CRL Sign
88+
X509v3 Subject Key Identifier:
89+
31:E8:52:DF:E1:49:F8:12:7B:7C:6E:E7:4E:91:7A:97:75:BC:A8:AE
90+
Signature Algorithm: sha256WithRSAEncryption
91+
8f:12:8e:8e:7a:fb:59:82:a8:0f:e6:be:b8:09:5d:17:c8:8e:
92+
c1:3a:c7:a4:52:d4:0d:2e:ac:a8:5c:b1:f4:52:ee:b7:c4:25:
93+
9a:2a:32:fc:91:3d:ba:29:9a:ed:c8:de:1f:75:39:54:16:d1:
94+
72:74:e0:95:a0:e2:41:36:9c:f8:95:c2:21:10:29:12:5f:4d:
95+
d1:b0:e1:a1:5b:c5:79:3c:d1:23:c9:c9:74:c2:42:58:fa:1b:
96+
35:75:77:30:7a:58:b2:07:e0:cd:ec:21:e2:51:54:59:08:21:
97+
be:c7:05:df:6e:55:81:21:0d:d1:ad:61:81:77:27:3e:bd:39:
98+
81:df:bd:91:32:3d:cc:5d:eb:de:fc:a7:73:26:2f:cd:88:a7:
99+
70:65:f4:35:06:b3:d6:02:56:e1:ba:e6:d5:6f:b0:4d:b5:95:
100+
cb:c6:34:a3:a7:35:79:99:bb:bf:cb:07:a0:d4:a0:de:f2:2c:
101+
e8:9b:27:43:c6:c0:5c:ae:62:da:a3:bf:01:76:50:bb:6e:70:
102+
1f:56:8f:41:cb:7c:41:d1:b0:c7:62:41:b2:31:23:99:6a:47:
103+
b8:10:c0:5c:f0:9e:b0:3e:5c:bb:d5:33:cc:38:1c:a5:dc:26:
104+
8b:b5:e2:76:5e:f8:92:3d:df:fc:78:2b:39:e8:a6:45:d3:9b:
105+
f2:51:b9:fc
106+
-----BEGIN CERTIFICATE-----
107+
MIIDjTCCAnWgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJVUzEL
108+
MAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdv
109+
b2dsZSBJbmMxHzAdBgNVBAMMFkhhd2tzYmlsbCBSb290IHYxIHByb2QwHhcNMTcw
110+
NzAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBoMQswCQYDVQQGEwJVUzELMAkGA1UE
111+
CAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2dsZSBJ
112+
bmMxHzAdBgNVBAMMFkhhd2tzYmlsbCBSb290IHYxIHByb2QwggEiMA0GCSqGSIb3
113+
DQEBAQUAA4IBDwAwggEKAoIBAQCsLqhiiSGgcJLfsI7Dk00mONulol9rHm2obCyD
114+
1lua+AKg+LAW+1zauZu5i028FSbgDk8vtSBDHDF+XsFnqTbIGV7Ctai2lnaQe1UV
115+
TVMWEPBi1diYGceeDrJpJqPz2aXTcIghrGISeyq+IC4z25uQp7G/D8AResKYqYxN
116+
NqcfZlMIk0s6Eh4aPyvCXYtLl9QXD0GDJ6nz4NmC+Fw31B5d5Kg9WXxDZOYC1zU5
117+
9JXbdxxzeC/EJo1k1AHghto/J8edvTIl5NQ0ahOHKoUZzhhDRsVBioFmymVuwaHO
118+
cXTUsHe3NTkNyeLIfoFpsQQ4XcH9kjO67YXTkdCWeNYw/FYZAgMBAAGjQjBAMA8G
119+
A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBQx6FLf4Un4
120+
Ent8budOkXqXdbyorjANBgkqhkiG9w0BAQsFAAOCAQEAjxKOjnr7WYKoD+a+uAld
121+
F8iOwTrHpFLUDS6sqFyx9FLut8Qlmioy/JE9uima7cjeH3U5VBbRcnTglaDiQTac
122+
+JXCIRApEl9N0bDhoVvFeTzRI8nJdMJCWPobNXV3MHpYsgfgzewh4lFUWQghvscF
123+
325VgSEN0a1hgXcnPr05gd+9kTI9zF3r3vyncyYvzYincGX0NQaz1gJW4brm1W+w
124+
TbWVy8Y0o6c1eZm7v8sHoNSg3vIs6JsnQ8bAXK5i2qO/AXZQu25wH1aPQct8QdGw
125+
x2JBsjEjmWpHuBDAXPCesD5cu9UzzDgcpdwmi7Xidl74kj3f/HgrOeimRdOb8lG5
126+
/A==
127+
-----END CERTIFICATE-----
128+
"""
129+
130+
131+
def get_manufacturer_root_certificate():
132+
"""Gets the manufacturer root certificate."""
133+
resp = requests.get(MANUFACTURER_CERT_URL)
134+
tmp_file = io.BytesIO(resp.content)
135+
zip_file = zipfile.ZipFile(tmp_file)
136+
with zip_file.open('liquid_security_certificate.crt') as f:
137+
return x509.load_pem_x509_certificate(f.read(), backends.default_backend())
138+
139+
140+
def get_owner_root_certificate():
141+
"""Gets the owner root certificate."""
142+
return x509.load_pem_x509_certificate(
143+
OWNER_ROOT_CERT_PEM.encode('utf-8'), backends.default_backend())
144+
145+
146+
def verify_certificate(signing_cert, issued_cert):
147+
"""Verifies the signing_cert issued the issued_cert.
148+
149+
Args:
150+
signing_cert: The certificate used to verify the issued certificate's
151+
signature.
152+
issued_cert: The issued certificate.
153+
154+
Returns:
155+
True if the signing_cert issued the issued_cert.
156+
"""
157+
if signing_cert.subject != issued_cert.issuer:
158+
return False
159+
try:
160+
signing_cert.public_key().verify(issued_cert.signature,
161+
issued_cert.tbs_certificate_bytes,
162+
padding.PKCS1v15(),
163+
issued_cert.signature_hash_algorithm)
164+
return True
165+
except exceptions.InvalidSignature:
166+
return False
167+
return False
168+
169+
170+
def get_issued_certificate(issuer_cert,
171+
untrusted_certs,
172+
predicate=lambda _: True):
173+
"""Finds an issued certificates issued by an issuer certificate.
174+
175+
The issued certificate is removed from the set of untrusted certificates.
176+
177+
Args:
178+
issuer_cert: The issuer certificate.
179+
untrusted_certs: A set of untrusted certificates.
180+
predicate: An additional condition for the issued certificate.
181+
182+
Returns:
183+
A certificate within the set of untrusted certificates that was issued
184+
by the issuer certificate and matches the predicate.
185+
"""
186+
for cert in untrusted_certs:
187+
if verify_certificate(issuer_cert, cert) and predicate(cert):
188+
untrusted_certs.remove(cert)
189+
return cert
190+
return None
191+
192+
193+
def verify_attestation(cert, attestation):
194+
"""Verifies that the certificate signed the attestation.
195+
196+
Args:
197+
cert: The certificate used to verify the attestation.
198+
attestation: The attestation to verify.
199+
200+
Returns:
201+
True if the certificate verified the attestation.
202+
"""
203+
data = attestation[:-ATTESTATION_SIGNATURE_LEN]
204+
signature = attestation[-ATTESTATION_SIGNATURE_LEN:]
205+
try:
206+
cert.public_key().verify(signature, data, padding.PKCS1v15(),
207+
hashes.SHA256())
208+
return True
209+
except exceptions.InvalidSignature:
210+
return False
211+
return False
212+
213+
214+
def verify(certs_file, attestation_file):
215+
"""Verifies that the certificate chains are valid.
216+
217+
Args:
218+
certs_file: The certificate chains filename.
219+
attestation_file: The attestation filename.
220+
221+
Returns:
222+
True if the certificate chains are valid.
223+
"""
224+
mfr_root_cert = get_manufacturer_root_certificate()
225+
if (mfr_root_cert.subject.public_bytes(backends.default_backend()) !=
226+
MANUFACTURER_CERT_SUBJECT_BYTES):
227+
return False
228+
229+
untrusted_certs_pem = pem.parse_file(certs_file)
230+
untrusted_certs = {
231+
x509.load_pem_x509_certificate(
232+
str(cert_pem).encode('utf-8'), backends.default_backend())
233+
for cert_pem in untrusted_certs_pem
234+
}
235+
236+
# Build the manufacturer certificate chain.
237+
mfr_card_cert = get_issued_certificate(mfr_root_cert, untrusted_certs)
238+
mfr_partition_cert = get_issued_certificate(mfr_card_cert, untrusted_certs)
239+
if not mfr_card_cert or not mfr_partition_cert:
240+
print('Invalid HSM manufacturer certificate chain.')
241+
return False
242+
print('Successfully built HSM manufacturer certificate chain.')
243+
244+
owner_root_cert = get_owner_root_certificate()
245+
246+
# Build the owner card certificate chain.
247+
def _check_card_pub_key(cert):
248+
cert_pub_key_bytes = cert.public_key().public_bytes(
249+
serialization.Encoding.DER,
250+
serialization.PublicFormat.SubjectPublicKeyInfo)
251+
mfr_card_pub_key_bytes = mfr_card_cert.public_key().public_bytes(
252+
serialization.Encoding.DER,
253+
serialization.PublicFormat.SubjectPublicKeyInfo)
254+
return cert_pub_key_bytes == mfr_card_pub_key_bytes
255+
256+
owner_card_cert = get_issued_certificate(
257+
owner_root_cert, untrusted_certs, predicate=_check_card_pub_key)
258+
259+
# Build the owner partition certificate chain.
260+
def _check_partition_pub_key(cert):
261+
cert_pub_key_bytes = cert.public_key().public_bytes(
262+
serialization.Encoding.PEM,
263+
serialization.PublicFormat.SubjectPublicKeyInfo)
264+
mfr_partition_pub_key_bytes = mfr_partition_cert.public_key().public_bytes(
265+
serialization.Encoding.PEM,
266+
serialization.PublicFormat.SubjectPublicKeyInfo)
267+
return cert_pub_key_bytes == mfr_partition_pub_key_bytes
268+
269+
owner_partition_cert = get_issued_certificate(
270+
owner_root_cert, untrusted_certs, predicate=_check_partition_pub_key)
271+
272+
if not owner_card_cert or not owner_partition_cert or untrusted_certs:
273+
print('Invalid HSM owner certificate chain.')
274+
return False
275+
print('Successfully built HSM owner certificate chain.')
276+
277+
with gzip.open(attestation_file, 'rb') as f:
278+
attestation = f.read()
279+
return (verify_attestation(mfr_partition_cert, attestation) and
280+
verify_attestation(owner_partition_cert, attestation))
281+
282+
283+
if __name__ == '__main__':
284+
parser = argparse.ArgumentParser(
285+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
286+
parser.add_argument(
287+
'--certificates', help='The certificate chains filename.')
288+
parser.add_argument('--attestation', help='The attestation filename.')
289+
290+
args = parser.parse_args()
291+
292+
if verify(args.certificates, args.attestation):
293+
print('The attestation has been verified.')
294+
else:
295+
print('The attestation could not be verified.')
296+
# [END kms_verify_chains]

0 commit comments

Comments
 (0)