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

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 json 

24import hashlib 

25import secp256k1 as ec 

26from .misc import info, head, AdminError 

27from .utils import is_nonempty_hex_string 

28from .certificate import HSMCertificate 

29 

30 

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 

38 

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" 

46 

47 

48def do_verify_attestation(options): 

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

50 

51 if options.attestation_certificate_file_path is None: 

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

53 

54 if options.pubkeys_file_path is None: 

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

56 

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

63 

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

74 

75 if type(pubkeys_map) != dict: 

76 raise ValueError( 

77 "Public keys file must contain an object as a top level element") 

78 

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

95 

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

99 

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

103 

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

109 

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) 

113 

114 # UI 

115 if "ui" not in result: 

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

117 

118 ui_result = result["ui"] 

119 if not ui_result[0]: 

120 raise AdminError(f"Invalid UI attestation: error validating '{ui_result[1]}'") 

121 

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

128 

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) 

141 

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 ) 

153 

154 # Signer 

155 if "signer" not in result: 

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

157 

158 signer_result = result["signer"] 

159 if not signer_result[0]: 

160 raise AdminError( 

161 f"Invalid Signer attestation: error validating '{signer_result[1]}'") 

162 

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

169 

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 ) 

176 

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 )