Coverage for tests/admin/test_attestation_utils.py: 100%

134 statements  

« 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. 

22 

23from types import SimpleNamespace 

24import secp256k1 as ec 

25from unittest import TestCase 

26from unittest.mock import patch, mock_open 

27from parameterized import parameterized 

28from admin.attestation_utils import AdminError, PowHsmAttestationMessage, load_pubkeys, \ 

29 compute_pubkeys_hash, compute_pubkeys_output, \ 

30 get_root_of_trust 

31from .test_attestation_utils_resources import TEST_PUBKEYS_JSON, \ 

32 TEST_PUBKEYS_JSON_INVALID 

33import logging 

34 

35logging.disable(logging.CRITICAL) 

36 

37 

38class TestPowHsmAttestationMessage(TestCase): 

39 @parameterized.expand([ 

40 ("ok_exact", True, b"POWHSM:5.6::"), 

41 ("ok_longer", True, b"POWHSM:5.3::whatcomesafterwards"), 

42 ("version_mismatch", False, b"POWHSM:4.3::"), 

43 ("shorter", False, b"POWHSM:5.3:"), 

44 ("invalid", False, b"something invalid"), 

45 ]) 

46 def test_is_header(self, _, expected, header): 

47 self.assertEqual(expected, PowHsmAttestationMessage.is_header(header)) 

48 

49 def test_parse_ok(self): 

50 msg = PowHsmAttestationMessage( 

51 b"POWHSM:5.7::" + 

52 b"abc" + 

53 bytes.fromhex("aa"*32) + 

54 bytes.fromhex("bb"*32) + 

55 bytes.fromhex("cc"*32) + 

56 bytes.fromhex("dd"*8) + 

57 bytes.fromhex("00"*7 + "83") 

58 ) 

59 

60 self.assertEqual("abc", msg.platform) 

61 self.assertEqual(bytes.fromhex("aa"*32), msg.ud_value) 

62 self.assertEqual(bytes.fromhex("bb"*32), msg.public_keys_hash) 

63 self.assertEqual(bytes.fromhex("cc"*32), msg.best_block) 

64 self.assertEqual(bytes.fromhex("dd"*8), msg.last_signed_tx) 

65 self.assertEqual(0x83, msg.timestamp) 

66 

67 def test_parse_header_mismatch(self): 

68 with self.assertRaises(ValueError) as e: 

69 PowHsmAttestationMessage( 

70 b"POWHSM:3.0::" + 

71 b"abc" + 

72 bytes.fromhex("aa"*32) + 

73 bytes.fromhex("bb"*32) + 

74 bytes.fromhex("cc"*32) + 

75 bytes.fromhex("dd"*8) + 

76 bytes.fromhex("00"*7 + "83") + 

77 b"0" 

78 ) 

79 self.assertIn("header", str(e.exception)) 

80 

81 def test_parse_shorter(self): 

82 with self.assertRaises(ValueError) as e: 

83 PowHsmAttestationMessage( 

84 b"POWHSM:5.7::" + 

85 b"abc" + 

86 bytes.fromhex("aa"*32) + 

87 bytes.fromhex("bb"*32) + 

88 bytes.fromhex("cc"*32) + 

89 bytes.fromhex("dd"*8) + 

90 bytes.fromhex("00"*6 + "83") 

91 ) 

92 self.assertIn("length mismatch", str(e.exception)) 

93 

94 def test_parse_longer(self): 

95 with self.assertRaises(ValueError) as e: 

96 PowHsmAttestationMessage( 

97 b"POWHSM:5.7::" + 

98 b"abc" + 

99 bytes.fromhex("aa"*32) + 

100 bytes.fromhex("bb"*32) + 

101 bytes.fromhex("cc"*32) + 

102 bytes.fromhex("dd"*8) + 

103 bytes.fromhex("00"*7 + "83") + 

104 b"0" 

105 ) 

106 self.assertIn("length mismatch", str(e.exception)) 

107 

108 

109class TestLoadPubKeys(TestCase): 

110 def test_load_pubkeys_ok(self): 

111 with patch("builtins.open", mock_open()) as file_mock: 

112 file_mock.return_value.read.return_value = TEST_PUBKEYS_JSON 

113 pubkeys = load_pubkeys("a-path") 

114 

115 file_mock.assert_called_with("a-path", "r") 

116 self.assertEqual([ 

117 "m/44'/1'/0'/0/0", 

118 "m/44'/1'/1'/0/0", 

119 "m/44'/1'/2'/0/0", 

120 ], list(pubkeys.keys())) 

121 self.assertEqual(bytes.fromhex( 

122 "03abe31ee7c91976f7a56d8e196d82d5ce75a0fcc2935723bf25610d22bd81e50f"), 

123 pubkeys["m/44'/1'/0'/0/0"].serialize(compressed=True)) 

124 self.assertEqual(bytes.fromhex( 

125 "03d44eac557a58be6cd4a40cbdaa9ed22cf4f0322e8c7bb84f6421d5bdda3b99ff"), 

126 pubkeys["m/44'/1'/1'/0/0"].serialize(compressed=True)) 

127 self.assertEqual(bytes.fromhex( 

128 "02877a756d2b82ddff342fa327b065326001b204b2f86a24ac36638b5162330141"), 

129 pubkeys["m/44'/1'/2'/0/0"].serialize(compressed=True)) 

130 

131 def test_load_pubkeys_file_doesnotexist(self): 

132 with patch("builtins.open", mock_open()) as file_mock: 

133 file_mock.side_effect = FileNotFoundError("another error") 

134 with self.assertRaises(AdminError) as e: 

135 load_pubkeys("a-path") 

136 file_mock.assert_called_with("a-path", "r") 

137 self.assertIn("another error", str(e.exception)) 

138 

139 def test_load_pubkeys_invalid_json(self): 

140 with patch("builtins.open", mock_open()) as file_mock: 

141 file_mock.return_value.read.return_value = "not json" 

142 with self.assertRaises(AdminError) as e: 

143 load_pubkeys("a-path") 

144 file_mock.assert_called_with("a-path", "r") 

145 self.assertIn("Unable to read", str(e.exception)) 

146 

147 def test_load_pubkeys_notamap(self): 

148 with patch("builtins.open", mock_open()) as file_mock: 

149 file_mock.return_value.read.return_value = "[1,2,3]" 

150 with self.assertRaises(AdminError) as e: 

151 load_pubkeys("a-path") 

152 file_mock.assert_called_with("a-path", "r") 

153 self.assertIn("top level", str(e.exception)) 

154 

155 def test_load_pubkeys_invalid_pubkey(self): 

156 with patch("builtins.open", mock_open()) as file_mock: 

157 file_mock.return_value.read.return_value = TEST_PUBKEYS_JSON_INVALID 

158 with self.assertRaises(AdminError) as e: 

159 load_pubkeys("a-path") 

160 file_mock.assert_called_with("a-path", "r") 

161 self.assertIn("public key", str(e.exception)) 

162 

163 

164class TestComputePubkeysHash(TestCase): 

165 def test_ok(self): 

166 expected_hash = bytes.fromhex( 

167 "ad33c8be1af2520e2c533d883a2021654102917969816cd1b9dacfcccf4e139e") 

168 

169 def to_pub(h): 

170 return ec.PrivateKey(bytes.fromhex(h), raw=True).pubkey 

171 

172 keys = { 

173 "1first": to_pub("11"*32), 

174 "3third": to_pub("33"*32), 

175 "2second": to_pub("22"*32), 

176 } 

177 

178 self.assertEqual(expected_hash, compute_pubkeys_hash(keys)) 

179 

180 def test_empty_errors(self): 

181 with self.assertRaises(AdminError) as e: 

182 compute_pubkeys_hash({}) 

183 self.assertIn("empty", str(e.exception)) 

184 

185 

186class TestComputePubkeysOutput(TestCase): 

187 def test_sample_output(self): 

188 class PubKey: 

189 def __init__(self, h): 

190 self.h = h 

191 

192 def serialize(self, compressed): 

193 return bytes.fromhex(self.h) if compressed else "" 

194 

195 keys = { 

196 "name": PubKey("11223344"), 

197 "longer_name": PubKey("aabbcc"), 

198 "very_very_long_name": PubKey("6677889900"), 

199 } 

200 

201 self.assertEqual([ 

202 "longer_name: aabbcc", 

203 "name: 11223344", 

204 "very_very_long_name: 6677889900", 

205 ], compute_pubkeys_output(keys)) 

206 

207 

208class TestGetRootOfTrust(TestCase): 

209 @patch("admin.attestation_utils.HSMCertificateV2ElementX509") 

210 @patch("admin.attestation_utils.Path") 

211 def test_file_ok(self, path, HSMCertificateV2ElementX509): 

212 path.return_value.is_file.return_value = True 

213 HSMCertificateV2ElementX509.from_pemfile.return_value = "the-result" 

214 

215 self.assertEqual("the-result", get_root_of_trust("a-file-path")) 

216 

217 path.assert_called_with("a-file-path") 

218 HSMCertificateV2ElementX509.from_pemfile.assert_called_with( 

219 "a-file-path", "sgx_root", "sgx_root") 

220 

221 @patch("admin.attestation_utils.HSMCertificateV2ElementX509") 

222 @patch("admin.attestation_utils.Path") 

223 def test_file_invalid(self, path, HSMCertificateV2ElementX509): 

224 path.return_value.is_file.return_value = True 

225 err = ValueError("something wrong") 

226 HSMCertificateV2ElementX509.from_pemfile.side_effect = err 

227 

228 with self.assertRaises(ValueError) as e: 

229 get_root_of_trust("a-file-path") 

230 self.assertEqual(err, e.exception) 

231 

232 path.assert_called_with("a-file-path") 

233 HSMCertificateV2ElementX509.from_pemfile.assert_called_with( 

234 "a-file-path", "sgx_root", "sgx_root") 

235 

236 @patch("admin.attestation_utils.requests") 

237 @patch("admin.attestation_utils.HSMCertificateV2ElementX509") 

238 @patch("admin.attestation_utils.Path") 

239 def test_url_ok(self, path, HSMCertificateV2ElementX509, requests): 

240 path.return_value.is_file.return_value = False 

241 requests.get.return_value = SimpleNamespace(**{ 

242 "status_code": 200, 

243 "content": b"some-pem", 

244 }) 

245 HSMCertificateV2ElementX509.from_pem.return_value = "the-result" 

246 

247 self.assertEqual("the-result", get_root_of_trust("a-url")) 

248 

249 path.assert_called_with("a-url") 

250 requests.get.assert_called_with("a-url") 

251 HSMCertificateV2ElementX509.from_pem.assert_called_with( 

252 "some-pem", "sgx_root", "sgx_root") 

253 

254 @patch("admin.attestation_utils.requests") 

255 @patch("admin.attestation_utils.HSMCertificateV2ElementX509") 

256 @patch("admin.attestation_utils.Path") 

257 def test_url_error_get(self, path, HSMCertificateV2ElementX509, requests): 

258 path.return_value.is_file.return_value = False 

259 requests.get.return_value = SimpleNamespace(**{ 

260 "status_code": 123, 

261 }) 

262 

263 with self.assertRaises(RuntimeError) as e: 

264 get_root_of_trust("a-url") 

265 self.assertIn("fetching root of trust", str(e.exception)) 

266 

267 path.assert_called_with("a-url") 

268 requests.get.assert_called_with("a-url") 

269 HSMCertificateV2ElementX509.from_pem.assert_not_called()