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

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 

23 

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 

29 

30logging.disable(logging.CRITICAL) 

31 

32 

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) 

38 

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

44 

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" 

48 

49 def setup_mocks(self, ec): 

50 ec.ECDSA.side_effect = lambda s: f"ecdsa-{s}" 

51 self.validator.get_crl_info = Mock() 

52 

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 

60 

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 

63 

64 def test_validate_ok(self, ec): 

65 self.setup_mocks(ec) 

66 self.mock_crl_info() 

67 

68 self.assertEqual({ 

69 "valid": True, 

70 "warnings": [], 

71 }, self.validator.validate(self.subject, self.issuer, datetime.now())) 

72 

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) 

84 

85 def test_validate_ok_no_crl_checking(self, ec): 

86 self.setup_mocks(ec) 

87 self.validator.get_crl_info = Mock() 

88 

89 self.assertEqual({ 

90 "valid": True, 

91 "warnings": [], 

92 }, self.validator.validate(self.subject, self.issuer, 

93 datetime.now(), check_crl=False)) 

94 

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

102 

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" 

107 

108 self.assertEqual({ 

109 "valid": True, 

110 "warnings": ["this is a CRL warning"], 

111 }, self.validator.validate(self.subject, self.issuer, datetime.now())) 

112 

113 def test_validate_invalid_certificates(self, ec): 

114 self.setup_mocks(ec) 

115 self.mock_crl_info() 

116 

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

121 

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

126 

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

130 

131 def test_validate_invalid_period(self, ec): 

132 self.setup_mocks(ec) 

133 self.mock_crl_info() 

134 

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

140 

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

146 

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

150 

151 def test_validate_not_issued_by_issuer(self, ec): 

152 self.setup_mocks(ec) 

153 self.mock_crl_info() 

154 

155 self.subject.verify_directly_issued_by.side_effect = RuntimeError("oops") 

156 

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

161 

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

165 

166 def test_validate_signature_invalid(self, ec): 

167 self.setup_mocks(ec) 

168 self.mock_crl_info() 

169 

170 self.issuer.public_key.return_value.verify.side_effect = RuntimeError("oopsies") 

171 

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

176 

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

184 

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

189 

190 self.assertEqual({ 

191 "valid": False, 

192 "reason": "Error gathering CRL info", 

193 }, self.validator.validate(self.subject, self.issuer, datetime.now())) 

194 

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

204 

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 

209 

210 self.assertEqual({ 

211 "valid": False, 

212 "reason": "Invalid CRL signature from the issuer", 

213 }, self.validator.validate(self.subject, self.issuer, datetime.now())) 

214 

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

225 

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 

230 

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

235 

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) 

247 

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

252 

253 self.assertEqual({ 

254 "valid": True, 

255 "warnings": [], 

256 }, self.validator.validate(self.subject, self.issuer, datetime.now())) 

257 

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) 

269 

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

276 

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

282 

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) 

294 

295 

296class TestX509CertificateValidatorGetCRLInfo(TestCase): 

297 def setUp(self): 

298 self.crl_getter = Mock() 

299 self.validator = X509CertificateValidator(self.crl_getter) 

300 

301 self.crldp_ext = Mock() 

302 

303 self.subject = Mock() 

304 self.subject.extensions.get_extension_for_class.return_value = self.crldp_ext 

305 

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

309 

310 self.crl_getter.return_value = "crl-for-url-1" 

311 

312 self.assertEqual("crl-for-url-1", self.validator.get_crl_info(self.subject)) 

313 

314 self.crl_getter.assert_called_once() 

315 self.crl_getter.assert_called_with("url-1") 

316 

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] 

322 

323 def getter(url): 

324 self.assertRegex(url, "^url-[123]$") 

325 if url[-1] == "3": 

326 return "crl-for-url-3" 

327 

328 raise RuntimeError("Unable to retrieve") 

329 

330 self.crl_getter.side_effect = getter 

331 

332 self.assertEqual("crl-for-url-3", self.validator.get_crl_info(self.subject)) 

333 

334 self.assertEqual(3, self.crl_getter.call_count) 

335 

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] 

341 

342 self.crl_getter.side_effect = RuntimeError("Unable to retrieve") 

343 

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

347 

348 self.assertEqual(3, self.crl_getter.call_count) 

349 

350 def test_get_crl_info_no_crldp(self): 

351 self.crldp_ext.value = [] 

352 

353 self.crl_getter.side_effect = RuntimeError("Unable to retrieve") 

354 

355 with self.assertRaises(RuntimeError) as e: 

356 self.validator.get_crl_info(self.subject) 

357 self.assertIn("No CRL distribution", str(e.exception)) 

358 

359 self.crl_getter.assert_not_called() 

360 

361 def test_get_crl_info_no_crldp_ext(self): 

362 self.subject.extensions.get_extension_for_class.side_effect = RuntimeError("oops") 

363 

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

368 

369 self.crl_getter.assert_not_called()