Coverage for admin/verify_ledger_attestation.py: 90%
93 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-07-10 13:43 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-07-10 13:43 +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 re
24from .misc import info, head, AdminError
25from .attestation_utils import PowHsmAttestationMessage, load_pubkeys, \
26 compute_pubkeys_hash, compute_pubkeys_output
27from .utils import is_nonempty_hex_string
28from .certificate import HSMCertificate, HSMCertificateRoot
31UI_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:UI:([2345].[0-9])")
32SIGNER_LEGACY_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:SIGNER:([2345].[0-9])")
33UI_DERIVATION_PATH = "m/44'/0'/0'/0/0"
34UD_VALUE_LENGTH = 32
35PUBLIC_KEYS_HASH_LENGTH = 32
36PUBKEY_COMPRESSED_LENGTH = 33
37SIGNER_HASH_LENGTH = 32
38SIGNER_ITERATION_LENGTH = 2
40# Ledger's root authority
41# (according to
42# https://github.com/LedgerHQ/blue-loader-python/blob/master/ledgerblue/
43# endorsementSetup.py#L138)
44DEFAULT_ROOT_AUTHORITY = "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f8"\
45 "18057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75"\
46 "dad609"
49def do_verify_attestation(options):
50 head("### -> Verify UI and Signer attestations", fill="#")
52 if options.attestation_certificate_file_path is None:
53 raise AdminError("No attestation certificate file given")
55 if options.pubkeys_file_path is None:
56 raise AdminError("No public keys file given")
58 root_authority = DEFAULT_ROOT_AUTHORITY
59 if options.root_authority is not None:
60 if not is_nonempty_hex_string(options.root_authority):
61 raise AdminError("Invalid root authority")
62 root_authority = options.root_authority
63 try:
64 root_authority = HSMCertificateRoot(root_authority)
65 except ValueError:
66 raise AdminError("Invalid root authority")
67 info(f"Using {root_authority} as root authority")
69 # Load public keys, compute their hash and format them for output
70 pubkeys_map = load_pubkeys(options.pubkeys_file_path)
71 pubkeys_hash = compute_pubkeys_hash(pubkeys_map)
72 pubkeys_output = compute_pubkeys_output(pubkeys_map)
74 # Find the expected UI public key
75 expected_ui_public_key = next(filter(
76 lambda pair: pair[0] == UI_DERIVATION_PATH, pubkeys_map.items()), (None, None))[1]
77 if expected_ui_public_key is None:
78 raise AdminError(
79 f"Public key with path {UI_DERIVATION_PATH} not present in public key file")
80 expected_ui_public_key = expected_ui_public_key.serialize(compressed=True).hex()
82 # Load the given attestation key certificate
83 try:
84 att_cert = HSMCertificate.from_jsonfile(options.attestation_certificate_file_path)
85 except Exception as e:
86 raise AdminError(f"While loading the attestation certificate file: {str(e)}")
88 # Validate the certificate using the given root authority
89 # (this should be *one of* Ledger's public keys)
90 result = att_cert.validate_and_get_values(root_authority)
92 # UI
93 if "ui" not in result:
94 raise AdminError("Certificate does not contain a UI attestation")
96 ui_result = result["ui"]
97 if not ui_result[0]:
98 raise AdminError(f"Invalid UI attestation: error validating '{ui_result[1]}'")
100 ui_message = bytes.fromhex(ui_result[1])
101 ui_hash = bytes.fromhex(ui_result[2])
102 mh_match = UI_MESSAGE_HEADER_REGEX.match(ui_message)
103 if mh_match is None:
104 raise AdminError(
105 f"Invalid UI attestation message header: {ui_message.hex()}")
106 mh_len = len(mh_match.group(0))
108 # Extract UI version, UD value, UI public key and signer version from message
109 ui_version = mh_match.group(1)
110 ud_value = ui_message[mh_len:mh_len + UD_VALUE_LENGTH].hex()
111 ui_public_key = ui_message[mh_len + UD_VALUE_LENGTH:mh_len + UD_VALUE_LENGTH +
112 PUBKEY_COMPRESSED_LENGTH].hex()
113 signer_hash = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH:
114 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
115 SIGNER_HASH_LENGTH].hex()
116 signer_iteration = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
117 SIGNER_HASH_LENGTH:
118 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
119 SIGNER_HASH_LENGTH + SIGNER_ITERATION_LENGTH]
120 signer_iteration = int.from_bytes(signer_iteration, byteorder='big', signed=False)
122 if ui_public_key != expected_ui_public_key:
123 raise AdminError("Invalid UI attestation: unexpected public key reported. "
124 f"Expected {expected_ui_public_key} but got {ui_public_key}")
126 head(
127 [
128 "UI verified with:",
129 f"UD value: {ud_value}",
130 f"Derived public key ({UI_DERIVATION_PATH}): {ui_public_key}",
131 f"Authorized signer hash: {signer_hash}",
132 f"Authorized signer iteration: {signer_iteration}",
133 f"Installed UI hash: {ui_hash.hex()}",
134 f"Installed UI version: {ui_version.decode()}",
135 ],
136 fill="-",
137 )
139 # Signer
140 if "signer" not in result:
141 raise AdminError("Certificate does not contain a Signer attestation")
143 signer_result = result["signer"]
144 if not signer_result[0]:
145 raise AdminError(
146 f"Invalid Signer attestation: error validating '{signer_result[1]}'")
148 signer_message = bytes.fromhex(signer_result[1])
149 signer_hash = bytes.fromhex(signer_result[2])
150 lmh_match = SIGNER_LEGACY_MESSAGE_HEADER_REGEX.match(signer_message)
151 if lmh_match is None and not PowHsmAttestationMessage.is_header(signer_message):
152 raise AdminError(
153 f"Invalid Signer attestation message header: {signer_message.hex()}")
155 if lmh_match is not None:
156 # Legacy header
157 powhsm_message = None
158 hlen = len(lmh_match.group(0))
159 signer_version = lmh_match.group(1).decode()
160 offset = hlen
161 reported_pubkeys_hash = signer_message[offset:]
162 offset += PUBLIC_KEYS_HASH_LENGTH
163 if signer_message[offset:] != b'':
164 raise AdminError(f"Signer attestation message longer "
165 f"than expected: {signer_message.hex()}")
166 else:
167 # New header
168 try:
169 powhsm_message = PowHsmAttestationMessage(signer_message, name="Signer")
170 except ValueError as e:
171 raise AdminError(str(e))
172 signer_version = powhsm_message.version
173 reported_pubkeys_hash = powhsm_message.public_keys_hash
175 # Validations on extracted values
176 if reported_pubkeys_hash != pubkeys_hash:
177 raise AdminError(
178 f"Signer attestation public keys hash mismatch: expected {pubkeys_hash.hex()}"
179 f" but attestation reports {reported_pubkeys_hash.hex()}"
180 )
182 signer_info = [
183 f"Hash: {pubkeys_hash.hex()}",
184 "",
185 f"Installed Signer hash: {signer_hash.hex()}",
186 f"Installed Signer version: {signer_version}",
187 ]
189 if powhsm_message is not None:
190 signer_info += [
191 f"Platform: {powhsm_message.platform}",
192 f"UD value: {powhsm_message.ud_value.hex()}",
193 f"Best block: {powhsm_message.best_block.hex()}",
194 f"Last transaction signed: {powhsm_message.last_signed_tx.hex()}",
195 f"Timestamp: {powhsm_message.timestamp}",
196 ]
198 head(
199 ["Signer verified with public keys:"] + pubkeys_output + signer_info,
200 fill="-",
201 )