|
| 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