diff --git a/certvalidator/context.py b/certvalidator/context.py index 2f64d00..616e316 100644 --- a/certvalidator/context.py +++ b/certvalidator/context.py @@ -84,8 +84,14 @@ class ValidationContext(): # period of certificates moment = None + # When using cached OCSP and CRL responses it is necessary to check their + # validity against the time which they were acquired. This time is assumed + # to be identical to the passed in moment. This value will only be set to + # moment if a moment is given and either a CRL or OCSP list is also given. + revocation_moment = None + # By default, any CRLs or OCSP responses that are passed to the constructor - # are chccked. If _allow_fetching is True, any CRLs or OCSP responses that + # are checked. If _allow_fetching is True, any CRLs or OCSP responses that # can be downloaded will also be checked. The next two attributes change # that behavior. @@ -134,11 +140,13 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, be ignored. :param moment: - If certificate validation should be performed based on a date and - time other than right now. A datetime.datetime object with a tzinfo - value. If this parameter is specified, then the only way to check - OCSP and CRL responses is to pass them via the crls and ocsps - parameters. Can not be combined with allow_fetching=True. + A datetime.datetime object with a tzinfo value. + To be used when certificate validation should be performed based on + a date and time other than right now. This is common when validating + signatures that include timestamps. Works in combination with + either passed OCSP and/or CRL responses *OR* allow_fetching=True, + but *NOT* both. If allow_fetching=True the certificate revocation + will be checked against the current time and not the passed moment. :param crls: None or a list/tuple of asn1crypto.crl.CertificateList objects of @@ -179,6 +187,7 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, considered weak. Valid options include: "md2", "md5", "sha1" """ + _revocation_moment = datetime.now(timezone.utc) if crls is not None: if not isinstance(crls, (list, tuple)): raise TypeError(pretty_message( @@ -227,15 +236,7 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, new_ocsps.append(ocsp_) ocsps = new_ocsps - if moment is not None: - if allow_fetching: - raise ValueError(pretty_message( - ''' - allow_fetching must be False when moment is specified - ''' - )) - - elif not allow_fetching and crls is None and ocsps is None and revocation_mode != "soft-fail": + if not allow_fetching and crls is None and ocsps is None and revocation_mode != "soft-fail": raise ValueError(pretty_message( ''' revocation_mode is "%s" and allow_fetching is False, however @@ -278,6 +279,15 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, attribute is not set to a valid timezone ''' )) + if moment is not None and (crls or ocsps): + if allow_fetching: + raise ValueError(pretty_message( + ''' + allow_fetching must be False when moment and (OCSPs or CRLs) + are specified + ''' + )) + _revocation_moment = moment if revocation_mode not in set(['soft-fail', 'hard-fail', 'require']): raise ValueError(pretty_message( @@ -336,6 +346,7 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, ) self.moment = moment + self.revocation_moment = _revocation_moment self._validate_map = {} self._crl_issuer_map = {} @@ -363,6 +374,13 @@ def __init__(self, trust_roots=None, extra_trust_roots=None, other_certs=None, self._soft_fail_exceptions = [] self.weak_hash_algos = weak_hash_algos + @property + def allow_fetching(self): + """ + Check if allow fetching is allowed + """ + return self._allow_fetching + @property def crls(self): """ @@ -468,7 +486,11 @@ def retrieve_crls(self, cert): self._fetched_crls[cert.issuer_serial] = [] if self._revocation_mode == "soft-fail": self._soft_fail_exceptions.append(e) - raise SoftFailError() + if hasattr(e, 'reason'): + if isinstance(e.reason, str): + raise SoftFailError(e.reason, [e.reason]) + else: + raise SoftFailError(str(e.reason), [str(e.reason)]) else: raise @@ -507,7 +529,13 @@ def retrieve_ocsps(self, cert, issuer): self._fetched_ocsps[cert.issuer_serial] = [] if self._revocation_mode == "soft-fail": self._soft_fail_exceptions.append(e) - raise SoftFailError() + if hasattr(e, 'reason'): + if isinstance(e.reason, str): + raise SoftFailError(e.reason, [e.reason]) + else: + raise SoftFailError(str(e.reason), [str(e.reason)]) + else: + raise SoftFailError(e.strerror, [e.strerror]) else: raise diff --git a/certvalidator/errors.py b/certvalidator/errors.py index 253c725..3995506 100644 --- a/certvalidator/errors.py +++ b/certvalidator/errors.py @@ -53,7 +53,9 @@ def failures(self): class SoftFailError(Exception): - pass + @property + def failures(self): + return self.args[1] class ValidationError(Exception): diff --git a/certvalidator/ocsp_client.py b/certvalidator/ocsp_client.py index 0e04c17..f3e839b 100644 --- a/certvalidator/ocsp_client.py +++ b/certvalidator/ocsp_client.py @@ -96,6 +96,10 @@ def fetch(cert, issuer, hash_algo='sha1', nonce=True, user_agent=None, timeout=1 response = urlopen(request, ocsp_request.dump(), timeout) ocsp_response = ocsp.OCSPResponse.load(response.read()) request_nonce = ocsp_request.nonce_value + if ocsp_response['response_status'].native == 'unauthorized': + raise errors.OCSPNoMatchesError( + 'Unable to verify OCSP response since the responder returned unauthorized' + ) response_nonce = ocsp_response.nonce_value if request_nonce and response_nonce and request_nonce.native != response_nonce.native: raise errors.OCSPValidationError( diff --git a/certvalidator/validate.py b/certvalidator/validate.py index 860cf3b..0bde6a5 100644 --- a/certvalidator/validate.py +++ b/certvalidator/validate.py @@ -1,7 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function +from datetime import datetime -from asn1crypto import x509, crl +from asn1crypto import x509, crl, util from oscrypto import asymmetric import oscrypto.errors @@ -365,12 +366,11 @@ def _validate_path(validation_context, path, end_entity_name_override=None): # Step 2 a 3 - CRL/OCSP if not validation_context._skip_revocation_checks: - status_good = False - revocation_check_failed = False - matched = False + status_good = None + revocation_check_failed = None + matched = None soft_fail = False failures = [] - if cert.ocsp_urls or validation_context.revocation_mode == 'require': try: verify_ocsp_response( @@ -391,7 +391,8 @@ def _validate_path(validation_context, path, end_entity_name_override=None): failures.extend([failure[0] for failure in e.failures]) revocation_check_failed = True matched = True - except (SoftFailError): + except (SoftFailError) as e: + failures.extend([failure[0] for failure in e.failures]) soft_fail = True except (OCSPNoMatchesError): pass @@ -413,12 +414,23 @@ def _validate_path(validation_context, path, end_entity_name_override=None): failures.extend([failure[0] for failure in e.failures]) revocation_check_failed = True matched = True - except (SoftFailError): + except (SoftFailError) as e: + failures.extend([failure[0] for failure in e.failures]) soft_fail = True except (CRLNoMatchesError): pass - if not soft_fail: + if soft_fail: + if validation_context.revocation_mode != 'soft-fail': + raise PathValidationError(pretty_message( + ''' + The path could not be validated because the %s revocation + checks failed: %s + ''', + _cert_type(index, last_index, end_entity_name_override), + '; '.join(failures) + )) + else: if not matched and validation_context.revocation_mode == 'require': raise PathValidationError(pretty_message( ''' @@ -842,7 +854,6 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, certvalidator.errors.OCSPValidationIndeterminateError - when the OCSP response could not be verified certvalidator.errors.RevokedError - when the OCSP response indicates the certificate has been revoked """ - if not isinstance(cert, x509.Certificate): raise TypeError(pretty_message( ''' @@ -880,7 +891,10 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, type_name(cert_description) )) - moment = validation_context.moment + if validation_context.allow_fetching: + moment = datetime.now(util.timezone.utc) + else: + moment = validation_context.revocation_moment issuer = path.find_issuer(cert) certificate_registry = validation_context.certificate_registry @@ -889,11 +903,11 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, mismatch_failures = 0 ocsp_responses = validation_context.retrieve_ocsps(cert, issuer) - for ocsp_response in ocsp_responses: # Make sure that we get a valid response back from the OCSP responder status = ocsp_response['response_status'].native + if status != 'successful': mismatch_failures += 1 continue @@ -953,12 +967,14 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, )) continue - if moment > cert_response['next_update'].native: - failures.append(( - 'OCSP response is from before the validation time', - ocsp_response - )) - continue + # according to RFC 2560 4.2.1 next_update is expected, but optional + if cert_response['next_update']: + if moment > cert_response['next_update'].native: + failures.append(( + 'OCSP response is from before the validation time', + ocsp_response + )) + continue # To verify the response as legitimate, the responder cert must be located if tbs_response['responder_id'].name == 'by_key': @@ -993,7 +1009,8 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, skip_ocsp = skip_ocsp or signing_cert_path == path if skip_ocsp and validation_context._skip_revocation_checks is False: changed_revocation_flags = True - + original_moment = validation_context.moment + validation_context.moment = moment original_revocation_mode = validation_context.revocation_mode new_revocation_mode = "soft-fail" if original_revocation_mode == "soft-fail" else "hard-fail" @@ -1017,6 +1034,7 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, if changed_revocation_flags: validation_context._skip_revocation_checks = False validation_context._revocation_mode = original_revocation_mode + validation_context.moment = original_moment else: failures.append(( @@ -1093,6 +1111,7 @@ def verify_ocsp_response(cert, path, validation_context, cert_description=None, # Finally check to see if the certificate has been revoked status = cert_response['cert_status'].name + if status == 'good': return @@ -1208,7 +1227,11 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description type_name(cert_description) )) - moment = validation_context.moment + if validation_context.allow_fetching: + moment = datetime.now(util.timezone.utc) + else: + moment = validation_context.revocation_moment + certificate_registry = validation_context.certificate_registry certificate_lists = validation_context.retrieve_crls(cert) @@ -1279,6 +1302,7 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description checked_reasons = set() failures = [] + soft_fail = False issuer_failures = 0 while len(crls_to_process) > 0: @@ -1348,16 +1372,17 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description # Step f candidate_crl_issuer_path = None - if validation_context: - candidate_crl_issuer_path = validation_context.check_validation(candidate_crl_issuer) + candidate_crl_issuer_path = validation_context.check_validation(candidate_crl_issuer) if candidate_crl_issuer_path is None: candidate_crl_issuer_path = path.copy().truncate_to_issuer(candidate_crl_issuer) candidate_crl_issuer_path.append(candidate_crl_issuer) try: + original_moment = validation_context.moment + validation_context.moment = moment + # Pre-emptively mark a path as validated to prevent recursion - if validation_context: - validation_context.record_validation(candidate_crl_issuer, candidate_crl_issuer_path) + validation_context.record_validation(candidate_crl_issuer, candidate_crl_issuer_path) temp_override = end_entity_name_override if temp_override is None and candidate_crl_issuer.sha256 != cert_issuer.sha256: @@ -1370,14 +1395,15 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description except (PathValidationError) as e: # If the validation did not work out, clear it - if validation_context: - validation_context.clear_validation(candidate_crl_issuer) + validation_context.clear_validation(candidate_crl_issuer) # We let a revoked error fall through since step k will catch # it with a correct error message if isinstance(e, RevokedError): raise raise CRLValidationError('CRL issuer certificate path could not be validated') + finally: + validation_context.moment = original_moment key_usage_value = candidate_crl_issuer.key_usage_value if key_usage_value and 'crl_sign' not in key_usage_value.native: @@ -1450,12 +1476,21 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description certificate_list )) continue - if moment > certificate_list['tbs_cert_list']['next_update'].native: - failures.append(( - 'CRL should have been regenerated by the validation time', - certificate_list - )) - continue + + # according to RFC 5280 5.1.2.5 next_update is expected, but optional + # in x.509 asn1 + if certificate_list['tbs_cert_list']['next_update']: + if moment > certificate_list['tbs_cert_list']['next_update'].native: + failures.append(( + 'CRL should have been regenerated by the validation time', + certificate_list + )) + continue + else: + failures.append( + ('nextUpdate field is expected to be present in CRL', + certificate_list)) + soft_fail = True # Step b 2 @@ -1709,8 +1744,7 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description # CRLs should not include this value, but at least one of the examples # from the NIST test suite does checked_reasons -= set(['unused']) - - if checked_reasons != valid_reasons: + if checked_reasons != valid_reasons or soft_fail: if total_crls == issuer_failures: raise CRLNoMatchesError(pretty_message( ''' @@ -1725,6 +1759,17 @@ def verify_crl(cert, path, validation_context, use_deltas=True, cert_description 'The available CRLs do not cover all revocation reasons', )) + if soft_fail: + raise SoftFailError( + pretty_message( + ''' + There was an issue with a CRL response for %s + ''', + cert_description + ), + failures + ) + raise CRLValidationIndeterminateError( pretty_message( ''' diff --git a/tests/fixtures/microsoft_armored.crt b/tests/fixtures/microsoft_armored.crt new file mode 100644 index 0000000..5346fa2 --- /dev/null +++ b/tests/fixtures/microsoft_armored.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEsDCCA5igAwIBAgIKYQIwfgAAAAAABjANBgkqhkiG9w0BAQUFADCBjjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx +HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE4MDYGA1UEAxMvTWljcm9z +b2Z0IFdpbmRvd3MgVmVyaWZpY2F0aW9uIEludGVybWVkaWF0ZSBQQ0EwHhcNMDgw +MzEwMjE1NzUxWhcNMDkwNjEwMjIwNzUxWjCBhDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv +c29mdCBDb3Jwb3JhdGlvbjEuMCwGA1UEAxMlTWljcm9zb2Z0IFdpbmRvd3MgQ29t +cG9uZW50IFB1Ymxpc2hlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AM6eB7sfwU+7EESoAhq9CEaTFZvluSCBvVeSFXeb7bf8E3bhjAUuRQ8KhnJTZqEe +Jv51GKvb8xqKqJ9jK7eEJPeTKyt1gt9/eUTCPUMVfOq8oNilm3ru8EqIb22srCmS +q14UgpnIKKUKQWTzbmh7isAVBS8d6l9Y6JME8G6xYXLj3jqOduljjDjIYcT0W1cN +oaex98IfM1RZaxM1oW813BsCMMes9K0rOBK0I4RNHCkPcd81DNAfzxIKm0YOy1bU +kUUWNsPC9cgS6kw3EFImraLZBTRqx5GdsjgBBo8160SoIzpzqLdu5pRr0rDnwNKE +wbtVys1jgGeVVUGR9+vE+10CAwEAAaOCARYwggESMA4GA1UdDwEB/wQEAwIGwDAd +BgNVHQ4EFgQUgqJI4gV8dQJa878Usv4M54MAQ08wHwYDVR0lBBgwFgYIKwYBBQUH +AwMGCisGAQQBgjcKAwYwHwYDVR0jBBgwFoAUi71bM+FBDLv6QnJ2V2+6gyVTes4w +SAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j +cmwvcHJvZHVjdHMvV2luSW50UENBLmNybDBVBggrBgEFBQcBAQRJMEcwRQYIKwYB +BQUHMAKGOWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z +b2Z0V2luSW50UENBLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAtLB0fkonhqHzUNNm +26yIAlEyZHLPtiRuY5EwQrJSYFr5pd+qWBFKdY27WEyK0pIjPnucoGsEB6k1aRZ8 +5EVXMlzaHgNshJHWrXJQ/bwz9MdZFEqSa48fR+GtY6Pc3XvJXAkHvSTP5s3RJm5h +ilw0V63ghpM+UnlJ7wML3P7k8wD7ZSZjNBHDqJmNfAAbn4wzHEPBkXhUZDwAokeo +LYoWPi51rUMRxy/yqTJC/TxRsY21i03F8nYqsBc4JKfIHSaYvKJ/bk/I3W5Xy4Gx +kegSvMkfZ9DavJnUpexKapM4+W9u2y1z/yOdM17Z2JeH6lKyv+BX53yIIVxGc/6k +/7bBew== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/root_certs/microsoft_intermediate.crt b/tests/fixtures/root_certs/microsoft_intermediate.crt new file mode 100644 index 0000000..1b3799b --- /dev/null +++ b/tests/fixtures/root_certs/microsoft_intermediate.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEtzCCA6OgAwIBAgIQaguZT8AAG6sR2jqhtt/siDAJBgUrDgMCHQUAMHAxKzAp +BgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNV +BAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJv +b3QgQXV0aG9yaXR5MB4XDTA1MTAxMTIxNTUyMFoXDTEwMDQyNjA3MDAwMFowgY4x +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt +b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xODA2BgNVBAMTL01p +Y3Jvc29mdCBXaW5kb3dzIFZlcmlmaWNhdGlvbiBJbnRlcm1lZGlhdGUgUENBMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxuPRRHfkSdaAtOnadcQsVyzw +YS/qskW72RVEc3Xrkv+sUWbu2DeXOet56TVW5N4YxnkcpYjLNTKv6HaywKY7kGTF +cWWV/GPlXyVtBq7xBUMOK7rybwmufB5KXpINR69KT9WyzjM28m3YMUOd5X9TyXP+ +GgevW4hZ/RO8M9rBRvnkq1yXlrJHqj5Wa6le9NO4UudltqWQLO2RwNvRJv26e/Qz +TFM41i6DZRpkuJmSxxvKC/aXYnOM3K8wKIk/wSG9DE/8pHtC6Wb9jbnTptgTVwVs +wam82wS1gOv1l6zxcrgnAF2o2WkyNaHZHZW4oPRMpronOt5fUUp1VbMAhX20RwID +AQABo4IBNDCCATAwHwYDVR0lBBgwFgYIKwYBBQUHAwMGCisGAQQBgjcKAwYwgaIG +A1UdAQSBmjCBl4AQW9Bw72lyniNRfhSyTY7/y6FyMHAxKzApBgNVBAsTIkNvcHly +aWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29m +dCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5 +gg8AwQCLPDyIEdE+9mPs30AwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFIu9 +WzPhQQy7+kJydldvuoMlU3rOMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsG +A1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MAkGBSsOAwIdBQADggEBAFvplmiV +UYXqzbvWTu0NGgXuJ4cNsTlc8n2fjxtHa5tqApuqNattSEIKcDKYttQgahGszaDG +4SjDkkWySbBFkUjkdWuwzcleObnjqjJc6xKMehxjQHNrojX1CWo2+gO/ZeZnevBc +tMVZ9ncgc4NfTygd0RBaw2cGtbG9NyNU0KJZN90OynQcn/HE9d90VgaizmIUc3M9 +/rKlksVW0EF6JkSgyy5mhVwNKVPJ5i1jMXHKnca5YOfqsJOi2jZSzrN9wwk7r0qz +3V4r57d2Mibs4L8mY4upiRXU7thYoSLWCBhKqdwvpNC//cTZpiilbz1sI05Yf90q +4O+kWiHU6pn4OEk= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/root_certs/microsoft_root.crt b/tests/fixtures/root_certs/microsoft_root.crt new file mode 100644 index 0000000..425f48b --- /dev/null +++ b/tests/fixtures/root_certs/microsoft_root.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIPAMEAizw8iBHRPvZj7N9AMA0GCSqGSIb3DQEBBAUAMHAx +KzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAc +BgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0 +IFJvb3QgQXV0aG9yaXR5MB4XDTk3MDExMDA3MDAwMFoXDTIwMTIzMTA3MDAwMFow +cDErMCkGA1UECxMiQ29weXJpZ2h0IChjKSAxOTk3IE1pY3Jvc29mdCBDb3JwLjEe +MBwGA1UECxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEwHwYDVQQDExhNaWNyb3Nv +ZnQgUm9vdCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCpAr3BcOY78k4bKJ+XeF4w6qKpjSVf+P6VTKO3/p2iID58UaKboo9gMmvRQmR5 +7qx2yVTa8uuchhyPn4Rms8VremIj1h083g8BkuiWxL8tZpqaaCaZ0Dosvwy1WCbB +RucKPjiWLKkoOajsSYNC44QPu5psVWGsgnyhYC13TOmZtGQ7mlAcMQgkFJ+p55Er +GOY9mGMUYFgFZZ8dN1KH96fvlALGG9O/VUWziYC/OuxUlE6u/ad6bXROrxjMlgko +IQBXkGBpN7tLEgc8Vv9b+6RmCgim0oFWV++2O14WgXcE2va+roCV/rDNf9anGnJc +PMq88AijIjCzBoXJsyB3E4XfAgMBAAGjgagwgaUwgaIGA1UdAQSBmjCBl4AQW9Bw +72lyniNRfhSyTY7/y6FyMHAxKzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBN +aWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEh +MB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5gg8AwQCLPDyIEdE+9mPs +30AwDQYJKoZIhvcNAQEEBQADggEBAJXoC8CN85cYNe24ASTYdxHzXGAyn54Lyz4F +kYiPyTrmIfLwV5MstaBHyGLv/NfMOztaqTZUaf4kbT/JzKreBXzdMY09nxBwarv+ +Ek8YacD80EPjEVogT+pie6+qGcgrNyUtvmWhEoolD2Oj91Qc+SHJ1hXzUqxuQzIH +/YIX+OVnbA1R9r3xUse958Qw/CAxCYgdlSkaTdUdAqXxgOADtFv0sd3IV+5lScdS +VLa0AygS/5DW8AiPfriXxas3LOR65Kh343agANBqP8HSNorgQRKoNWobats14dQc +BOSoRQTIWjM4bk0cDWK3CqKM09VUP0bNHFWmcNsSOoeTdZ+n0qA= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/test_certificate_validator.py b/tests/test_certificate_validator.py index 5608eb3..216d981 100644 --- a/tests/test_certificate_validator.py +++ b/tests/test_certificate_validator.py @@ -32,6 +32,14 @@ def _load_cert_object(self, *path_components): cert = x509.Certificate.load(cert_bytes) return cert + def _load_trust_roots(self, path): + rootCertificates = [] + certificates = os.listdir(path) + certificate_files = [cert for cert in certificates if '.crt' in cert] + for certificate_file_name in certificate_files: + rootCertificates.append(self._load_cert_object('root_certs', certificate_file_name)) + return rootCertificates + def test_basic_certificate_validator_tls(self): cert = self._load_cert_object('codex.crt') other_certs = [self._load_cert_object('GeoTrust_EV_SSL_CA_-_G4.crt')] @@ -92,3 +100,22 @@ def test_basic_certificate_validator_tls_whitelist(self): # If whitelist does not work, this will raise exception for key usage validator.validate_usage(set(['crl_sign'])) + + def test_crl_without_update_field(self): + cert = self._load_cert_object('microsoft_armored.crt') + root_certificates = self._load_trust_roots(os.path.join(fixtures_dir, 'root_certs')) + moment = datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + context = ValidationContext(trust_roots=root_certificates, moment=moment, + allow_fetching=True) + validator = CertificateValidator(cert, validation_context=context) + validator.validate_usage(set(['digital_signature']), set(['code_signing']), False) + + def test_crl_without_update_field_hard_fail(self): + cert = self._load_cert_object('microsoft_armored.crt') + root_certificates = self._load_trust_roots(os.path.join(fixtures_dir, 'root_certs')) + moment = datetime(2009, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + context = ValidationContext(trust_roots=root_certificates, moment=moment, + allow_fetching=True, revocation_mode='hard-fail') + validator = CertificateValidator(cert, validation_context=context) + with self.assertRaisesRegexp(PathValidationError, 'nextUpdate field is expected to be present in CRL'): + validator.validate_usage(set(['digital_signature']), set(['code_signing']), False) \ No newline at end of file