Coverage for admin/verify_ledger_attestation.py: 90%
93 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +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["valid"]:
98 raise AdminError(f"Invalid UI attestation: error "
99 f"validating '{ui_result["failed_element"]}'")
101 ui_message = bytes.fromhex(ui_result["value"])
102 ui_hash = bytes.fromhex(ui_result["tweak"])
103 mh_match = UI_MESSAGE_HEADER_REGEX.match(ui_message)
104 if mh_match is None:
105 raise AdminError(
106 f"Invalid UI attestation message header: {ui_message.hex()}")
107 mh_len = len(mh_match.group(0))
109 # Extract UI version, UD value, UI public key and signer version from message
110 ui_version = mh_match.group(1)
111 ud_value = ui_message[mh_len:mh_len + UD_VALUE_LENGTH].hex()
112 ui_public_key = ui_message[mh_len + UD_VALUE_LENGTH:mh_len + UD_VALUE_LENGTH +
113 PUBKEY_COMPRESSED_LENGTH].hex()
114 signer_hash = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH:
115 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
116 SIGNER_HASH_LENGTH].hex()
117 signer_iteration = ui_message[mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
118 SIGNER_HASH_LENGTH:
119 mh_len + UD_VALUE_LENGTH + PUBKEY_COMPRESSED_LENGTH +
120 SIGNER_HASH_LENGTH + SIGNER_ITERATION_LENGTH]
121 signer_iteration = int.from_bytes(signer_iteration, byteorder='big', signed=False)
123 if ui_public_key != expected_ui_public_key:
124 raise AdminError("Invalid UI attestation: unexpected public key reported. "
125 f"Expected {expected_ui_public_key} but got {ui_public_key}")
127 head(
128 [
129 "UI verified with:",
130 f"UD value: {ud_value}",
131 f"Derived public key ({UI_DERIVATION_PATH}): {ui_public_key}",
132 f"Authorized signer hash: {signer_hash}",
133 f"Authorized signer iteration: {signer_iteration}",
134 f"Installed UI hash: {ui_hash.hex()}",
135 f"Installed UI version: {ui_version.decode()}",
136 ],
137 fill="-",
138 )
140 # Signer
141 if "signer" not in result:
142 raise AdminError("Certificate does not contain a Signer attestation")
144 signer_result = result["signer"]
145 if not signer_result["valid"]:
146 raise AdminError(
147 f"Invalid Signer attestation: error "
148 f"validating '{signer_result["failed_element"]}'")
150 signer_message = bytes.fromhex(signer_result["value"])
151 signer_hash = bytes.fromhex(signer_result["tweak"])
152 lmh_match = SIGNER_LEGACY_MESSAGE_HEADER_REGEX.match(signer_message)
153 if lmh_match is None and not PowHsmAttestationMessage.is_header(signer_message):
154 raise AdminError(
155 f"Invalid Signer attestation message header: {signer_message.hex()}")
157 if lmh_match is not None:
158 # Legacy header
159 powhsm_message = None
160 hlen = len(lmh_match.group(0))
161 signer_version = lmh_match.group(1).decode()
162 offset = hlen
163 reported_pubkeys_hash = signer_message[offset:]
164 offset += PUBLIC_KEYS_HASH_LENGTH
165 if signer_message[offset:] != b'':
166 raise AdminError(f"Signer attestation message longer "
167 f"than expected: {signer_message.hex()}")
168 else:
169 # New header
170 try:
171 powhsm_message = PowHsmAttestationMessage(signer_message, name="Signer")
172 except ValueError as e:
173 raise AdminError(str(e))
174 signer_version = powhsm_message.version
175 reported_pubkeys_hash = powhsm_message.public_keys_hash
177 # Validations on extracted values
178 if reported_pubkeys_hash != pubkeys_hash:
179 raise AdminError(
180 f"Signer attestation public keys hash mismatch: expected {pubkeys_hash.hex()}"
181 f" but attestation reports {reported_pubkeys_hash.hex()}"
182 )
184 signer_info = [
185 f"Hash: {pubkeys_hash.hex()}",
186 "",
187 f"Installed Signer hash: {signer_hash.hex()}",
188 f"Installed Signer version: {signer_version}",
189 ]
191 if powhsm_message is not None:
192 signer_info += [
193 f"Platform: {powhsm_message.platform}",
194 f"UD value: {powhsm_message.ud_value.hex()}",
195 f"Best block: {powhsm_message.best_block.hex()}",
196 f"Last transaction signed: {powhsm_message.last_signed_tx.hex()}",
197 f"Timestamp: {powhsm_message.timestamp}",
198 ]
200 head(
201 ["Signer verified with public keys:"] + pubkeys_output + signer_info,
202 fill="-",
203 )