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

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. 

22 

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 

29 

30 

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 

39 

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" 

47 

48 

49def do_verify_attestation(options): 

50 head("### -> Verify UI and Signer attestations", fill="#") 

51 

52 if options.attestation_certificate_file_path is None: 

53 raise AdminError("No attestation certificate file given") 

54 

55 if options.pubkeys_file_path is None: 

56 raise AdminError("No public keys file given") 

57 

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

68 

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) 

73 

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

81 

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)}") 

87 

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) 

91 

92 # UI 

93 if "ui" not in result: 

94 raise AdminError("Certificate does not contain a UI attestation") 

95 

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"]}'") 

100 

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

108 

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) 

122 

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}") 

126 

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 ) 

139 

140 # Signer 

141 if "signer" not in result: 

142 raise AdminError("Certificate does not contain a Signer attestation") 

143 

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"]}'") 

149 

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()}") 

156 

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 

176 

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 ) 

183 

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 ] 

190 

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 ] 

199 

200 head( 

201 ["Signer verified with public keys:"] + pubkeys_output + signer_info, 

202 fill="-", 

203 )