Coverage for tests/admin/test_x509_validator.py: 100%
183 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
« 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.
24from datetime import datetime, timedelta
25from unittest import TestCase
26from unittest.mock import patch, Mock, MagicMock
27from admin.x509_validator import X509CertificateValidator, x509
28import logging
30logging.disable(logging.CRITICAL)
33@patch("admin.x509_validator.ec")
34class TestX509CertificateValidatorValidate(TestCase):
35 def setUp(self):
36 self.crl_getter = Mock()
37 self.validator = X509CertificateValidator(self.crl_getter)
39 self.subject = MagicMock(spec=x509.Certificate)
40 self.subject.subject = "the subject"
41 self.issuer = MagicMock(spec=x509.Certificate)
42 self.issuer.subject = "the issuer"
43 self.issuer.public_key.return_value = Mock()
45 self.subject.not_valid_before_utc = datetime.now() - timedelta(days=100)
46 self.subject.not_valid_after_utc = datetime.now() + timedelta(days=100)
47 self.subject.signature_hash_algorithm = "the-algorithm"
49 def setup_mocks(self, ec):
50 ec.ECDSA.side_effect = lambda s: f"ecdsa-{s}"
51 self.validator.get_crl_info = Mock()
53 def mock_crl_info(self):
54 self.crl_info = {
55 "crl": Mock(),
56 "issuer_chain": None,
57 "warning": None,
58 }
59 self.validator.get_crl_info.return_value = self.crl_info
61 self.crl_info["crl"].is_signature_valid.return_value = True
62 self.crl_info["crl"].get_revoked_certificate_by_serial_number.return_value = None
64 def test_validate_ok(self, ec):
65 self.setup_mocks(ec)
66 self.mock_crl_info()
68 self.assertEqual({
69 "valid": True,
70 "warnings": [],
71 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
73 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
74 self.issuer.public_key.return_value.verify.assert_called_with(
75 self.subject.signature,
76 self.subject.tbs_certificate_bytes,
77 "ecdsa-the-algorithm"
78 )
79 self.validator.get_crl_info.assert_called_with(self.subject)
80 self.crl_info["crl"].is_signature_valid.assert_called_with(
81 self.issuer.public_key())
82 self.crl_info["crl"].get_revoked_certificate_by_serial_number.\
83 assert_called_with(self.subject.serial_number)
85 def test_validate_ok_no_crl_checking(self, ec):
86 self.setup_mocks(ec)
87 self.validator.get_crl_info = Mock()
89 self.assertEqual({
90 "valid": True,
91 "warnings": [],
92 }, self.validator.validate(self.subject, self.issuer,
93 datetime.now(), check_crl=False))
95 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
96 self.issuer.public_key.return_value.verify.assert_called_with(
97 self.subject.signature,
98 self.subject.tbs_certificate_bytes,
99 "ecdsa-the-algorithm"
100 )
101 self.validator.get_crl_info.assert_not_called()
103 def test_validate_ok_with_crl_warnings(self, ec):
104 self.setup_mocks(ec)
105 self.mock_crl_info()
106 self.crl_info["warning"] = "this is a CRL warning"
108 self.assertEqual({
109 "valid": True,
110 "warnings": ["this is a CRL warning"],
111 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
113 def test_validate_invalid_certificates(self, ec):
114 self.setup_mocks(ec)
115 self.mock_crl_info()
117 self.assertEqual({
118 "valid": False,
119 "reason": "Both subject and issuer must be instances of x509.Certificate",
120 }, self.validator.validate("not a certificate", self.issuer, datetime.now()))
122 self.assertEqual({
123 "valid": False,
124 "reason": "Both subject and issuer must be instances of x509.Certificate",
125 }, self.validator.validate(self.subject, "not a certificate", datetime.now()))
127 self.subject.verify_directly_issued_by.assert_not_called()
128 self.issuer.public_key.return_value.verify.assert_not_called()
129 self.validator.get_crl_info.assert_not_called()
131 def test_validate_invalid_period(self, ec):
132 self.setup_mocks(ec)
133 self.mock_crl_info()
135 self.assertEqual({
136 "valid": False,
137 "reason": "the subject not within validity period",
138 }, self.validator.validate(self.subject, self.issuer,
139 datetime.now() - timedelta(days=101)))
141 self.assertEqual({
142 "valid": False,
143 "reason": "the subject not within validity period",
144 }, self.validator.validate(self.subject, self.issuer,
145 datetime.now() + timedelta(days=101)))
147 self.subject.verify_directly_issued_by.assert_not_called()
148 self.issuer.public_key.return_value.verify.assert_not_called()
149 self.validator.get_crl_info.assert_not_called()
151 def test_validate_not_issued_by_issuer(self, ec):
152 self.setup_mocks(ec)
153 self.mock_crl_info()
155 self.subject.verify_directly_issued_by.side_effect = RuntimeError("oops")
157 self.assertEqual({
158 "valid": False,
159 "reason": "Verifying the subject issued by the issuer: oops",
160 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
162 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
163 self.issuer.public_key.return_value.verify.assert_not_called()
164 self.validator.get_crl_info.assert_not_called()
166 def test_validate_signature_invalid(self, ec):
167 self.setup_mocks(ec)
168 self.mock_crl_info()
170 self.issuer.public_key.return_value.verify.side_effect = RuntimeError("oopsies")
172 self.assertEqual({
173 "valid": False,
174 "reason": "Verifying the subject issued by the issuer: oopsies",
175 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
177 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
178 self.issuer.public_key.return_value.verify.assert_called_with(
179 self.subject.signature,
180 self.subject.tbs_certificate_bytes,
181 "ecdsa-the-algorithm"
182 )
183 self.validator.get_crl_info.assert_not_called()
185 def test_validate_crl_info_gathering_error(self, ec):
186 self.setup_mocks(ec)
187 self.mock_crl_info()
188 self.validator.get_crl_info.side_effect = RuntimeError("Error gathering CRL info")
190 self.assertEqual({
191 "valid": False,
192 "reason": "Error gathering CRL info",
193 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
195 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
196 self.issuer.public_key.return_value.verify.assert_called_with(
197 self.subject.signature,
198 self.subject.tbs_certificate_bytes,
199 "ecdsa-the-algorithm"
200 )
201 self.validator.get_crl_info.assert_called_with(self.subject)
202 self.crl_info["crl"].is_signature_valid.assert_not_called()
203 self.crl_info["crl"].get_revoked_certificate_by_serial_number.assert_not_called()
205 def test_validate_crl_invalid_signature(self, ec):
206 self.setup_mocks(ec)
207 self.mock_crl_info()
208 self.crl_info["crl"].is_signature_valid.return_value = False
210 self.assertEqual({
211 "valid": False,
212 "reason": "Invalid CRL signature from the issuer",
213 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
215 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
216 self.issuer.public_key.return_value.verify.assert_called_with(
217 self.subject.signature,
218 self.subject.tbs_certificate_bytes,
219 "ecdsa-the-algorithm"
220 )
221 self.validator.get_crl_info.assert_called_with(self.subject)
222 self.crl_info["crl"].is_signature_valid.assert_called_with(
223 self.issuer.public_key())
224 self.crl_info["crl"].get_revoked_certificate_by_serial_number.assert_not_called()
226 def test_validate_subject_revoked(self, ec):
227 self.setup_mocks(ec)
228 self.mock_crl_info()
229 self.crl_info["crl"].get_revoked_certificate_by_serial_number.return_value = 123
231 self.assertEqual({
232 "valid": False,
233 "reason": "the subject found in the issuer CRL",
234 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
236 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
237 self.issuer.public_key.return_value.verify.assert_called_with(
238 self.subject.signature,
239 self.subject.tbs_certificate_bytes,
240 "ecdsa-the-algorithm"
241 )
242 self.validator.get_crl_info.assert_called_with(self.subject)
243 self.crl_info["crl"].is_signature_valid.assert_called_with(
244 self.issuer.public_key())
245 self.crl_info["crl"].get_revoked_certificate_by_serial_number.\
246 assert_called_with(self.subject.serial_number)
248 def test_validate_ok_with_crl_issuer_chain(self, ec):
249 self.setup_mocks(ec)
250 self.mock_crl_info()
251 self.crl_info["issuer_chain"] = [self.issuer, "something", "else"]
253 self.assertEqual({
254 "valid": True,
255 "warnings": [],
256 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
258 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
259 self.issuer.public_key.return_value.verify.assert_called_with(
260 self.subject.signature,
261 self.subject.tbs_certificate_bytes,
262 "ecdsa-the-algorithm"
263 )
264 self.validator.get_crl_info.assert_called_with(self.subject)
265 self.crl_info["crl"].is_signature_valid.assert_called_with(
266 self.issuer.public_key())
267 self.crl_info["crl"].get_revoked_certificate_by_serial_number.\
268 assert_called_with(self.subject.serial_number)
270 def test_validate_error_crl_issuer_chain_leaf_mismatch(self, ec):
271 self.setup_mocks(ec)
272 self.mock_crl_info()
273 leaf = Mock()
274 leaf.subject = "different subject"
275 self.crl_info["issuer_chain"] = [leaf, "something", "else"]
277 self.assertEqual({
278 "valid": False,
279 "reason": "CRL issuer chain leaf different subject does not match "
280 "certificate the subject issuer the issuer",
281 }, self.validator.validate(self.subject, self.issuer, datetime.now()))
283 self.subject.verify_directly_issued_by.assert_called_with(self.issuer)
284 self.issuer.public_key.return_value.verify.assert_called_with(
285 self.subject.signature,
286 self.subject.tbs_certificate_bytes,
287 "ecdsa-the-algorithm"
288 )
289 self.validator.get_crl_info.assert_called_with(self.subject)
290 self.crl_info["crl"].is_signature_valid.assert_called_with(
291 self.issuer.public_key())
292 self.crl_info["crl"].get_revoked_certificate_by_serial_number.\
293 assert_called_with(self.subject.serial_number)
296class TestX509CertificateValidatorGetCRLInfo(TestCase):
297 def setUp(self):
298 self.crl_getter = Mock()
299 self.validator = X509CertificateValidator(self.crl_getter)
301 self.crldp_ext = Mock()
303 self.subject = Mock()
304 self.subject.extensions.get_extension_for_class.return_value = self.crldp_ext
306 def test_get_crl_info_ok_first_url(self):
307 crldp_1 = Mock(full_name=[Mock(value="url-1")])
308 self.crldp_ext.value = [crldp_1, "doesnt-matter", "neither"]
310 self.crl_getter.return_value = "crl-for-url-1"
312 self.assertEqual("crl-for-url-1", self.validator.get_crl_info(self.subject))
314 self.crl_getter.assert_called_once()
315 self.crl_getter.assert_called_with("url-1")
317 def test_get_crl_info_ok_third_url(self):
318 crldp_1 = Mock(full_name=[Mock(value="url-1")])
319 crldp_2 = Mock(full_name=[Mock(value="url-2")])
320 crldp_3 = Mock(full_name=[Mock(value="url-3")])
321 self.crldp_ext.value = [crldp_1, crldp_2, crldp_3]
323 def getter(url):
324 self.assertRegex(url, "^url-[123]$")
325 if url[-1] == "3":
326 return "crl-for-url-3"
328 raise RuntimeError("Unable to retrieve")
330 self.crl_getter.side_effect = getter
332 self.assertEqual("crl-for-url-3", self.validator.get_crl_info(self.subject))
334 self.assertEqual(3, self.crl_getter.call_count)
336 def test_get_crl_info_all_urls_error(self):
337 crldp_1 = Mock(full_name=[Mock(value="url-1")])
338 crldp_2 = Mock(full_name=[Mock(value="url-2")])
339 crldp_3 = Mock(full_name=[Mock(value="url-3")])
340 self.crldp_ext.value = [crldp_1, crldp_2, crldp_3]
342 self.crl_getter.side_effect = RuntimeError("Unable to retrieve")
344 with self.assertRaises(RuntimeError) as e:
345 self.validator.get_crl_info(self.subject)
346 self.assertIn("None of the distribution", str(e.exception))
348 self.assertEqual(3, self.crl_getter.call_count)
350 def test_get_crl_info_no_crldp(self):
351 self.crldp_ext.value = []
353 self.crl_getter.side_effect = RuntimeError("Unable to retrieve")
355 with self.assertRaises(RuntimeError) as e:
356 self.validator.get_crl_info(self.subject)
357 self.assertIn("No CRL distribution", str(e.exception))
359 self.crl_getter.assert_not_called()
361 def test_get_crl_info_no_crldp_ext(self):
362 self.subject.extensions.get_extension_for_class.side_effect = RuntimeError("oops")
364 with self.assertRaises(RuntimeError) as e:
365 self.validator.get_crl_info(self.subject)
366 self.assertIn("Unable to fetch", str(e.exception))
367 self.assertIn("oops", str(e.exception))
369 self.crl_getter.assert_not_called()