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

174 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 datetime import datetime, timedelta, UTC 

24from unittest import TestCase 

25from unittest.mock import Mock, patch 

26from ecdsa import NIST256p 

27from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding 

28from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 

29from admin.certificate_v2 import HSMCertificateV2Element, \ 

30 HSMCertificateV2ElementX509 

31from .test_certificate_v2_resources import TEST_CERTIFICATE 

32 

33 

34class TestHSMCertificateV2ElementX509(TestCase): 

35 def setUp(self): 

36 self.elem = HSMCertificateV2ElementX509({ 

37 "name": "thename", 

38 "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", 

39 "signed_by": "whosigned", 

40 }) 

41 

42 def test_props(self): 

43 self.assertEqual("thename", self.elem.name) 

44 self.assertEqual("whosigned", self.elem.signed_by) 

45 self.assertEqual("dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", self.elem.message) 

46 

47 def test_dict_ok(self): 

48 self.assertEqual({ 

49 "name": "thename", 

50 "type": "x509_pem", 

51 "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", 

52 "signed_by": "whosigned", 

53 }, self.elem.to_dict()) 

54 

55 def test_parse_identity(self): 

56 source = TEST_CERTIFICATE["elements"][3] 

57 elem = HSMCertificateV2Element.from_dict(source) 

58 self.assertTrue(isinstance(elem, HSMCertificateV2ElementX509)) 

59 self.assertEqual(source, elem.to_dict()) 

60 

61 def test_from_dict_invalid_message(self): 

62 with self.assertRaises(ValueError) as e: 

63 HSMCertificateV2Element.from_dict({ 

64 "name": "quoting_enclave", 

65 "type": "x509_pem", 

66 "message": "not-base-64", 

67 "signed_by": "platform_ca" 

68 }) 

69 self.assertIn("Invalid message", str(e.exception)) 

70 

71 def test_get_value_notimplemented(self): 

72 with self.assertRaises(NotImplementedError): 

73 self.elem.get_value() 

74 

75 def test_from_pem(self): 

76 self.assertEqual({ 

77 "name": "thename", 

78 "type": "x509_pem", 

79 "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", 

80 "signed_by": "whosigned", 

81 }, HSMCertificateV2ElementX509.from_pem(""" 

82 -----BEGIN CERTIFICATE----- 

83 dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl 

84 -----END CERTIFICATE----- 

85 """, "thename", "whosigned").to_dict()) 

86 

87 @patch("admin.certificate_v2.Path") 

88 @patch("admin.certificate_v2.HSMCertificateV2ElementX509.from_pem") 

89 def test_from_pemfile(self, from_pem, Path): 

90 Path.return_value.read_text.return_value = "the pem contents" 

91 from_pem.return_value = "the instance" 

92 self.assertEqual("the instance", 

93 HSMCertificateV2ElementX509.from_pemfile("a-file.pem", 

94 "the name", 

95 "who signed")) 

96 Path.assert_called_with("a-file.pem") 

97 from_pem.assert_called_with("the pem contents", 

98 "the name", 

99 "who signed") 

100 

101 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

102 def test_certificate(self, load_pem_x509_certificate): 

103 load_pem_x509_certificate.return_value = "mock-certificate" 

104 

105 self.assertEqual("mock-certificate", self.elem.certificate) 

106 self.assertEqual("mock-certificate", self.elem.certificate) 

107 

108 load_pem_x509_certificate.assert_called_with( 

109 b"-----BEGIN CERTIFICATE-----" 

110 b"dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl" 

111 b"-----END CERTIFICATE-----" 

112 ) 

113 self.assertEqual(1, load_pem_x509_certificate.call_count) 

114 

115 def setup_pubkey_mocks(self, load_pem_x509_certificate, VerifyingKey): 

116 self.pubkey = Mock() 

117 self.pubkey.curve = SECP256R1() 

118 self.pubkey.public_bytes.return_value = "the-public-bytes" 

119 self.cert = Mock() 

120 self.cert.public_key.return_value = self.pubkey 

121 load_pem_x509_certificate.return_value = self.cert 

122 VerifyingKey.from_string.return_value = "the-expected-pubkey" 

123 

124 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

125 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

126 def test_get_pubkey_ok(self, load_pem_x509_certificate, VerifyingKey): 

127 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

128 

129 self.assertEqual("the-expected-pubkey", self.elem.get_pubkey()) 

130 self.pubkey.public_bytes.assert_called_with( 

131 Encoding.X962, PublicFormat.CompressedPoint) 

132 VerifyingKey.from_string.assert_called_with("the-public-bytes", NIST256p) 

133 

134 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

135 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

136 def test_get_pubkey_err_load_cert(self, load_pem_x509_certificate, VerifyingKey): 

137 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

138 load_pem_x509_certificate.side_effect = Exception("blah blah") 

139 

140 with self.assertRaises(ValueError) as e: 

141 self.elem.get_pubkey() 

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

143 self.assertIn("blah blah", str(e.exception)) 

144 self.pubkey.public_bytes.assert_not_called() 

145 VerifyingKey.from_string.assert_not_called() 

146 

147 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

148 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

149 def test_get_pubkey_err_get_pub(self, load_pem_x509_certificate, VerifyingKey): 

150 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

151 self.cert.public_key.side_effect = Exception("blah blah") 

152 

153 with self.assertRaises(ValueError) as e: 

154 self.elem.get_pubkey() 

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

156 self.assertIn("blah blah", str(e.exception)) 

157 self.pubkey.public_bytes.assert_not_called() 

158 VerifyingKey.from_string.assert_not_called() 

159 

160 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

161 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

162 def test_get_pubkey_err_pub_notnistp256(self, load_pem_x509_certificate, 

163 VerifyingKey): 

164 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

165 self.pubkey.curve = "somethingelse" 

166 

167 with self.assertRaises(ValueError) as e: 

168 self.elem.get_pubkey() 

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

170 self.assertIn("NIST P-256", str(e.exception)) 

171 self.pubkey.public_bytes.assert_not_called() 

172 VerifyingKey.from_string.assert_not_called() 

173 

174 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

175 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

176 def test_get_pubkey_err_public_bytes(self, load_pem_x509_certificate, VerifyingKey): 

177 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

178 self.pubkey.public_bytes.side_effect = Exception("blah blah") 

179 

180 with self.assertRaises(ValueError) as e: 

181 self.elem.get_pubkey() 

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

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

184 self.pubkey.public_bytes.assert_called_with( 

185 Encoding.X962, PublicFormat.CompressedPoint) 

186 VerifyingKey.from_string.assert_not_called() 

187 

188 @patch("admin.certificate_v2.ecdsa.VerifyingKey") 

189 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

190 def test_get_pubkey_err_ecdsafromstring(self, load_pem_x509_certificate, 

191 VerifyingKey): 

192 self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) 

193 VerifyingKey.from_string.side_effect = Exception("blah blah") 

194 

195 with self.assertRaises(ValueError) as e: 

196 self.elem.get_pubkey() 

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

198 self.assertIn("blah blah", str(e.exception)) 

199 self.pubkey.public_bytes.assert_called_with( 

200 Encoding.X962, PublicFormat.CompressedPoint) 

201 VerifyingKey.from_string.assert_called_with("the-public-bytes", NIST256p) 

202 

203 def setup_is_valid_mocks(self, load_pem_x509_certificate, ec): 

204 self.certifier = HSMCertificateV2ElementX509({ 

205 "name": "mock-certifier", 

206 "signed_by": "someone-else", 

207 "message": "Y2VydGlmaWVy" 

208 }) 

209 

210 self.mock_certifier = Mock() 

211 self.mock_elem = Mock() 

212 

213 def load_mock(data): 

214 if b"Y2VydGlmaWVy" in data: 

215 return self.mock_certifier 

216 return self.mock_elem 

217 

218 load_pem_x509_certificate.side_effect = load_mock 

219 

220 self.now = datetime.now(UTC) 

221 one_week = timedelta(weeks=1) 

222 self.mock_elem.not_valid_before_utc = self.now - one_week 

223 self.mock_elem.not_valid_after_utc = self.now + one_week 

224 self.mock_certifier_pk = Mock() 

225 self.mock_certifier.public_key.return_value = self.mock_certifier_pk 

226 self.mock_elem.signature = "the-signature" 

227 self.mock_elem.tbs_certificate_bytes = "the-fingerprint" 

228 self.mock_elem.signature_hash_algorithm = "the-signature-hash-algo" 

229 ec.ECDSA.return_value = "the-ecdsa-algo" 

230 

231 @patch("admin.certificate_v2.ec") 

232 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

233 def test_is_valid_ok(self, load_pem_x509_certificate, ec): 

234 self.setup_is_valid_mocks(load_pem_x509_certificate, ec) 

235 

236 self.assertTrue(self.elem.is_valid(self.certifier)) 

237 

238 self.mock_certifier_pk.verify.assert_called_with( 

239 "the-signature", 

240 "the-fingerprint", 

241 "the-ecdsa-algo" 

242 ) 

243 ec.ECDSA.assert_called_with("the-signature-hash-algo") 

244 

245 @patch("admin.certificate_v2.ec") 

246 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

247 def test_is_valid_before_in_future(self, load_pem_x509_certificate, ec): 

248 self.setup_is_valid_mocks(load_pem_x509_certificate, ec) 

249 self.mock_elem.not_valid_before_utc = self.now + \ 

250 timedelta(minutes=1) 

251 

252 self.assertFalse(self.elem.is_valid(self.certifier)) 

253 

254 self.mock_certifier_pk.verify.assert_not_called() 

255 ec.ECDSA.assert_not_called() 

256 

257 @patch("admin.certificate_v2.ec") 

258 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

259 def test_is_valid_after_in_past(self, load_pem_x509_certificate, ec): 

260 self.setup_is_valid_mocks(load_pem_x509_certificate, ec) 

261 self.mock_elem.not_valid_after_utc = self.now - \ 

262 timedelta(minutes=1) 

263 

264 self.assertFalse(self.elem.is_valid(self.certifier)) 

265 

266 self.mock_certifier_pk.verify.assert_not_called() 

267 ec.ECDSA.assert_not_called() 

268 

269 @patch("admin.certificate_v2.ec") 

270 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

271 def test_is_valid_signature_invalid(self, load_pem_x509_certificate, ec): 

272 self.setup_is_valid_mocks(load_pem_x509_certificate, ec) 

273 self.mock_certifier_pk.verify.side_effect = RuntimeError("wrong signature") 

274 

275 self.assertFalse(self.elem.is_valid(self.certifier)) 

276 

277 self.mock_certifier_pk.verify.assert_called_with( 

278 "the-signature", 

279 "the-fingerprint", 

280 "the-ecdsa-algo" 

281 ) 

282 ec.ECDSA.assert_called_with("the-signature-hash-algo") 

283 

284 @patch("admin.certificate_v2.ec") 

285 @patch("admin.certificate_v2.x509.load_pem_x509_certificate") 

286 def test_is_valid_x509_error(self, load_pem_x509_certificate, ec): 

287 self.setup_is_valid_mocks(load_pem_x509_certificate, ec) 

288 load_pem_x509_certificate.side_effect = ValueError("a random error") 

289 

290 self.assertFalse(self.elem.is_valid(self.certifier)) 

291 

292 self.mock_certifier_pk.verify.assert_not_called() 

293 ec.ECDSA.assert_not_called()