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
« 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.
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
35logging.disable(logging.CRITICAL)
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))
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 )
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)
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))
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))
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))
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")
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))
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))
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))
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))
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))
164class TestComputePubkeysHash(TestCase):
165 def test_ok(self):
166 expected_hash = bytes.fromhex(
167 "ad33c8be1af2520e2c533d883a2021654102917969816cd1b9dacfcccf4e139e")
169 def to_pub(h):
170 return ec.PrivateKey(bytes.fromhex(h), raw=True).pubkey
172 keys = {
173 "1first": to_pub("11"*32),
174 "3third": to_pub("33"*32),
175 "2second": to_pub("22"*32),
176 }
178 self.assertEqual(expected_hash, compute_pubkeys_hash(keys))
180 def test_empty_errors(self):
181 with self.assertRaises(AdminError) as e:
182 compute_pubkeys_hash({})
183 self.assertIn("empty", str(e.exception))
186class TestComputePubkeysOutput(TestCase):
187 def test_sample_output(self):
188 class PubKey:
189 def __init__(self, h):
190 self.h = h
192 def serialize(self, compressed):
193 return bytes.fromhex(self.h) if compressed else ""
195 keys = {
196 "name": PubKey("11223344"),
197 "longer_name": PubKey("aabbcc"),
198 "very_very_long_name": PubKey("6677889900"),
199 }
201 self.assertEqual([
202 "longer_name: aabbcc",
203 "name: 11223344",
204 "very_very_long_name: 6677889900",
205 ], compute_pubkeys_output(keys))
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"
215 self.assertEqual("the-result", get_root_of_trust("a-file-path"))
217 path.assert_called_with("a-file-path")
218 HSMCertificateV2ElementX509.from_pemfile.assert_called_with(
219 "a-file-path", "sgx_root", "sgx_root")
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
228 with self.assertRaises(ValueError) as e:
229 get_root_of_trust("a-file-path")
230 self.assertEqual(err, e.exception)
232 path.assert_called_with("a-file-path")
233 HSMCertificateV2ElementX509.from_pemfile.assert_called_with(
234 "a-file-path", "sgx_root", "sgx_root")
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"
247 self.assertEqual("the-result", get_root_of_trust("a-url"))
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")
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 })
263 with self.assertRaises(RuntimeError) as e:
264 get_root_of_trust("a-url")
265 self.assertIn("fetching root of trust", str(e.exception))
267 path.assert_called_with("a-url")
268 requests.get.assert_called_with("a-url")
269 HSMCertificateV2ElementX509.from_pem.assert_not_called()