Coverage for admin/verify_attestation.py: 92%
84 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 20:41 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 20:41 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2021 RSK Labs Ltd
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy of
6# this software and associated documentation files (the "Software"), to deal in
7# the Software without restriction, including without limitation the rights to
8# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9# of the Software, and to permit persons to whom the Software is furnished to do
10# so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23import json
24import hashlib
25import secp256k1 as ec
26from .misc import info, head, AdminError
27from .utils import is_nonempty_hex_string
28from .certificate import HSMCertificate
31UI_MESSAGE_HEADER = b"HSM:UI:4.0"
32SIGNER_MESSAGE_HEADER = b"HSM:SIGNER:4.0"
33UI_DERIVATION_PATH = "m/44'/0'/0'/0/0"
34UD_VALUE_LENGTH = 32
35PUBKEY_COMPRESSED_LENGTH = 33
36SIGNER_HASH_LENGTH = 32
37SIGNER_ITERATION_LENGTH = 2
39# Ledger's root authority
40# (according to
41# https://github.com/LedgerHQ/blue-loader-python/blob/master/ledgerblue/
42# endorsementSetup.py#L138)
43DEFAULT_ROOT_AUTHORITY = "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f8"\
44 "18057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75"\
45 "dad609"
48def do_verify_attestation(options):
49 head("### -> Verify UI and Signer attestations", fill="#")
51 if options.attestation_certificate_file_path is None:
52 raise AdminError("No attestation certificate file given")
54 if options.pubkeys_file_path is None:
55 raise AdminError("No public keys file given")
57 root_authority = DEFAULT_ROOT_AUTHORITY
58 if options.root_authority is not None:
59 if not is_nonempty_hex_string(options.root_authority):
60 raise AdminError("Invalid root authority")
61 root_authority = options.root_authority
62 info(f"Using {root_authority} as root authority")
64 # Load the given public keys and compute
65 # their hash (sha256sum of the uncompressed
66 # public keys in lexicographical path order)
67 # Also find and save the public key corresponding
68 # to the expected derivation path for the UI
69 # attestation
70 expected_ui_public_key = None
71 try:
72 with open(options.pubkeys_file_path, "r") as file:
73 pubkeys_map = json.loads(file.read())
75 if type(pubkeys_map) != dict:
76 raise ValueError(
77 "Public keys file must contain an object as a top level element")
79 pubkeys_hash = hashlib.sha256()
80 pubkeys_output = []
81 path_name_padding = max(map(len, pubkeys_map.keys()))
82 for path in sorted(pubkeys_map.keys()):
83 pubkey = pubkeys_map[path]
84 if not is_nonempty_hex_string(pubkey):
85 raise AdminError(f"Invalid public key for path {path}: {pubkey}")
86 pubkey = ec.PublicKey(bytes.fromhex(pubkey), raw=True)
87 pubkeys_hash.update(pubkey.serialize(compressed=False))
88 pubkeys_output.append(
89 f"{(path + ':').ljust(path_name_padding+1)} "
90 f"{pubkey.serialize(compressed=True).hex()}"
91 )
92 if path == UI_DERIVATION_PATH:
93 expected_ui_public_key = pubkey.serialize(compressed=True).hex()
94 pubkeys_hash = pubkeys_hash.digest()
96 except (ValueError, json.JSONDecodeError) as e:
97 raise ValueError('Unable to read public keys from "%s": %s' %
98 (options.pubkeys_file_path, str(e)))
100 if expected_ui_public_key is None:
101 raise AdminError(
102 f"Public key with path {UI_DERIVATION_PATH} not present in public key file")
104 # Load the given attestation key certificate
105 try:
106 att_cert = HSMCertificate.from_jsonfile(options.attestation_certificate_file_path)
107 except Exception as e:
108 raise AdminError(f"While loading the attestation certificate file: {str(e)}")
110 # Validate the certificate using the given root authority
111 # (this should be *one of* Ledger's public keys)
112 result = att_cert.validate_and_get_values(root_authority)
114 # UI
115 if "ui" not in result:
116 raise AdminError("Certificate does not contain a UI attestation")
118 ui_result = result["ui"]
119 if not ui_result[0]:
120 raise AdminError(f"Invalid UI attestation: error validating '{ui_result[1]}'")
122 ui_message = bytes.fromhex(ui_result[1])
123 ui_hash = bytes.fromhex(ui_result[2])
124 mh_len = len(UI_MESSAGE_HEADER)
125 if ui_message[:mh_len] != UI_MESSAGE_HEADER:
126 raise AdminError(
127 f"Invalid UI attestation message header: {ui_message[:mh_len].hex()}")
129 # Extract UD value, UI public key and signer version from message
130 ud_value = ui_message[mh_len:mh_len + UD_VALUE_LENGTH].hex()
131 ui_public_key = ui_message[mh_len + UD_VALUE_LENGTH:mh_len + UD_VALUE_LENGTH +
132 PUBKEY_COMPRESSED_LENGTH].hex()
133 signer_hash = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH:
134 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
135 SIGNER_HASH_LENGTH].hex()
136 signer_iteration = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
137 SIGNER_HASH_LENGTH:
138 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
139 SIGNER_HASH_LENGTH + SIGNER_ITERATION_LENGTH]
140 signer_iteration = int.from_bytes(signer_iteration, byteorder='big', signed=False)
142 head(
143 [
144 "UI verified with:",
145 f"UD value: {ud_value}",
146 f"Derived public key ({UI_DERIVATION_PATH}): {ui_public_key}",
147 f"Authorized signer hash: {signer_hash}",
148 f"Authorized signer iteration: {signer_iteration}",
149 f"Installed UI hash: {ui_hash.hex()}",
150 ],
151 fill="-",
152 )
154 # Signer
155 if "signer" not in result:
156 raise AdminError("Certificate does not contain a Signer attestation")
158 signer_result = result["signer"]
159 if not signer_result[0]:
160 raise AdminError(
161 f"Invalid Signer attestation: error validating '{signer_result[1]}'")
163 signer_message = bytes.fromhex(signer_result[1])
164 signer_hash = bytes.fromhex(signer_result[2])
165 mh_len = len(SIGNER_MESSAGE_HEADER)
166 if signer_message[:mh_len] != SIGNER_MESSAGE_HEADER:
167 raise AdminError(
168 f"Invalid Signer attestation message header: {signer_message[:mh_len].hex()}")
170 if signer_message[mh_len:] != pubkeys_hash:
171 reported = signer_message[mh_len:].hex()
172 raise AdminError(
173 f"Signer attestation public keys hash mismatch: expected {pubkeys_hash.hex()}"
174 f" but attestation reports {reported}"
175 )
177 head(
178 ["Signer verified with public keys:"] + pubkeys_output + [
179 "",
180 f"Hash: {signer_message[mh_len:].hex()}",
181 f"Installed Signer hash: {signer_hash.hex()}",
182 ],
183 fill="-",
184 )