diff --git a/CHANGELOG.md b/CHANGELOG.md index 9599891..bdcfc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project from version 0.9.3 onwards are documented in this file. +## 0.11.5 - 2024-10-03 + +### New features/enhancements + +### Fixes + ## 0.11.4 - 2024-08-29 ### New features/enhancements diff --git a/VERSION.txt b/VERSION.txt index aa3545d..b799a8c 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.11.4 \ No newline at end of file +0.11.5 \ No newline at end of file diff --git a/pkilint/bin/lint_crl.py b/pkilint/bin/lint_crl.py index 5ef107f..73e7157 100644 --- a/pkilint/bin/lint_crl.py +++ b/pkilint/bin/lint_crl.py @@ -49,7 +49,7 @@ def main(cli_args=None) -> int: if args.profile == 'BR': doc_additional_validators.append( - cabf_crl.create_reason_code_validator(crl_type) + cabf_crl.CabfCrlReasonCodeAllowlistValidator(crl_type) ) validity_additional_validators.append( diff --git a/pkilint/cabf/cabf_crl.py b/pkilint/cabf/cabf_crl.py index a2b44f3..9b84edc 100644 --- a/pkilint/cabf/cabf_crl.py +++ b/pkilint/cabf/cabf_crl.py @@ -36,23 +36,25 @@ def create_validity_period_validator( ) -def create_reason_code_validator( - crl_type: crl.CertificateRevocationListType -): - allowed_reasons = [ - rfc5280.CRLReason.namedValues[r] - for r in [ - 'keyCompromise', - 'affiliationChanged', - 'superseded', - 'cessationOfOperation', - 'privilegeWithdrawn', +class CabfCrlReasonCodeAllowlistValidator(crl_extension.CrlReasonCodeAllowlistValidator): + VALIDATION_PROHIBITED_CRL_REASON_CODE = validation.ValidationFinding( + validation.ValidationFindingSeverity.ERROR, + 'cabf.crl_prohibited_reason_code' + ) + + def __init__(self, crl_type: crl.CertificateRevocationListType): + allowed_reasons = [ + rfc5280.CRLReason.namedValues[r] + for r in [ + 'keyCompromise', + 'affiliationChanged', + 'superseded', + 'cessationOfOperation', + 'privilegeWithdrawn', + ] ] - ] - if crl_type == crl.CertificateRevocationListType.ARL: - allowed_reasons.append(rfc5280.CRLReason.namedValues['cACompromise']) + if crl_type == crl.CertificateRevocationListType.ARL: + allowed_reasons.append(rfc5280.CRLReason.namedValues['cACompromise']) - return crl_extension.CrlReasonCodeAllowlistValidator( - allowed_reason_codes=allowed_reasons - ) + super().__init__(allowed_reasons, self.VALIDATION_PROHIBITED_CRL_REASON_CODE) diff --git a/pkilint/pkix/certificate/__init__.py b/pkilint/pkix/certificate/__init__.py index ab7cac4..bd5274c 100644 --- a/pkilint/pkix/certificate/__init__.py +++ b/pkilint/pkix/certificate/__init__.py @@ -293,15 +293,7 @@ def create_extensions_validator_container(additional_validators=None): certificate_extension.KeyUsageCriticalityValidator(), certificate_extension.KeyUsageValidator(), general_name.UriSyntaxValidator(pdu_class=rfc5280.CPSuri), - general_name.GeneralNameUriSyntaxValidator(), - general_name.GeneralNameDnsNameSyntaxValidator(), - general_name.GeneralNameIpAddressSyntaxValidator(), - general_name.GeneralNameMailboxAddressSyntaxValidator(), - general_name.SmtpUTF8MailboxValidator(), - general_name.GeneralNameDnsNameDomainNameLengthValidator(), - general_name.GeneralNameUriDomainNameLengthValidator(), - general_name.GeneralNameRfc822NameDomainNameLengthValidator(), - general_name.SmtpUTF8MailboxDomainNameLengthValidator(), + general_name.GeneralNameValidatorContainer(), certificate_extension.DuplicatePolicyValidator(), certificate_extension.CrlDpCriticalityValidator(), certificate_extension.NameConstraintsCriticalityValidator(), diff --git a/pkilint/pkix/crl/__init__.py b/pkilint/pkix/crl/__init__.py index 1a46ec8..60f3f6c 100644 --- a/pkilint/pkix/crl/__init__.py +++ b/pkilint/pkix/crl/__init__.py @@ -75,10 +75,9 @@ def create_validity_validator_container(additional_validators=None): additional_validators = [] return validation.ValidatorContainer( validators=[ - crl_validity.CrlSaneValidityPeriodValidator(), time.TimeCorrectEncodingValidator(), ] + additional_validators, - path='certificateList.tbsCertList' + pdu_class=rfc5280.Time ) @@ -116,12 +115,10 @@ def create_pkix_crl_validator_container( crl_extension.CrlReasonCodeCriticalityValidator(), time.UtcTimeCorrectSyntaxValidator(), time.GeneralizedTimeCorrectSyntaxValidator(), + crl_validity.CrlSaneValidityPeriodValidator(), pkix.CertificateSerialNumberValidator(), crl_extension.CrlNumberValueValidator(), - general_name.GeneralNameIpAddressSyntaxValidator(), - general_name.GeneralNameMailboxAddressSyntaxValidator(), - general_name.GeneralNameIpAddressSyntaxValidator(), - general_name.GeneralNameUriSyntaxValidator(), + general_name.GeneralNameValidatorContainer(), ] return validation.ValidatorContainer( diff --git a/pkilint/pkix/crl/crl_extension.py b/pkilint/pkix/crl/crl_extension.py index 8a7d474..c883bad 100644 --- a/pkilint/pkix/crl/crl_extension.py +++ b/pkilint/pkix/crl/crl_extension.py @@ -93,22 +93,18 @@ def __init__(self): class CrlReasonCodeAllowlistValidator(validation.Validator): - VALIDATION_PROHIBITED_CRL_REASON_CODE = validation.ValidationFinding( - validation.ValidationFindingSeverity.ERROR, - 'pkix.crl_prohibited_reason_code' - ) - - def __init__(self, *, allowed_reason_codes): + def __init__(self, allowed_reason_codes, prohibited_reason_code_validation: validation.ValidationFinding): self._allowed_reason_codes = allowed_reason_codes + self._prohibited_reason_code_validation = prohibited_reason_code_validation super().__init__( pdu_class=rfc5280.CRLReason, - validations=[self.VALIDATION_PROHIBITED_CRL_REASON_CODE] + validations=[self._prohibited_reason_code_validation] ) def validate(self, node): if node.pdu not in self._allowed_reason_codes: raise validation.ValidationFindingEncountered( - self.VALIDATION_PROHIBITED_CRL_REASON_CODE, + self._prohibited_reason_code_validation, f'Prohibited reason code "{node.pdu}"' ) diff --git a/pkilint/pkix/crl/crl_validity.py b/pkilint/pkix/crl/crl_validity.py index a173921..f891a6e 100644 --- a/pkilint/pkix/crl/crl_validity.py +++ b/pkilint/pkix/crl/crl_validity.py @@ -11,6 +11,6 @@ class CrlSaneValidityPeriodValidator(time.SaneValidityPeriodValidator): def __init__(self): super().__init__( end_validity_node_retriever=lambda n: n.navigate('^.nextUpdate'), - path='certificateList.tbsCertificateList.thisUpdate', + path='certificateList.tbsCertList.thisUpdate', validation=self.VALIDATION_NEGATIVE_VALIDITY_PERIOD ) diff --git a/pkilint/pkix/general_name.py b/pkilint/pkix/general_name.py index 2aceb1b..e2908e0 100644 --- a/pkilint/pkix/general_name.py +++ b/pkilint/pkix/general_name.py @@ -48,6 +48,24 @@ def create_generalname_type_predicate(generalname_type): raise ValueError(f'Invalid type specified: {generalname_type}') +class GeneralNameValidatorContainer(validation.ValidatorContainer): + def __init__(self): + super().__init__( + validators=[ + GeneralNameUriSyntaxValidator(), + GeneralNameDnsNameSyntaxValidator(), + GeneralNameIpAddressSyntaxValidator(), + GeneralNameMailboxAddressSyntaxValidator(), + SmtpUTF8MailboxValidator(), + GeneralNameDnsNameDomainNameLengthValidator(), + GeneralNameUriDomainNameLengthValidator(), + GeneralNameRfc822NameDomainNameLengthValidator(), + SmtpUTF8MailboxDomainNameLengthValidator(), + ], + pdu_class=rfc5280.GeneralName + ) + + class UriSyntaxValidator(validation.Validator): VALIDATION_INVALID_URI_SYNTAX = validation.ValidationFinding( validation.ValidationFindingSeverity.ERROR, diff --git a/pkilint/pkix/time.py b/pkilint/pkix/time.py index e145cbe..fed803f 100644 --- a/pkilint/pkix/time.py +++ b/pkilint/pkix/time.py @@ -85,18 +85,15 @@ class TimeCorrectEncodingValidator(validation.Validator): def __init__(self): super().__init__(pdu_class=rfc5280.Time, validations=[ - VALIDATION_INVALID_TIME_SYNTAX, self.VALIDATION_WRONG_TIME_TYPE, ]) def validate(self, node): try: parsed_datetime = parse_time_node(node) - except ValueError as e: - raise validation.ValidationFindingEncountered( - VALIDATION_INVALID_TIME_SYNTAX, - str(e) - ) + except ValueError: + # the time syntax validator will report any errors + return if ('generalTime' in node.children) and ( parsed_datetime.year >= 1950) and ( diff --git a/tests/integration_certificate/__init__.py b/tests/integration_certificate/__init__.py index e4c5e07..c425b35 100644 --- a/tests/integration_certificate/__init__.py +++ b/tests/integration_certificate/__init__.py @@ -1,96 +1,23 @@ -import csv -import io -import typing -from os import path +import functools from pathlib import Path -from pkilint import loader, validation, finding_filter as result_filter +from pkilint import loader +from tests import integration_test_common _FIXTURE_DIR = Path(__file__).parent.resolve() _CERT_END_ASCII_ARMOR = '-----END CERTIFICATE-----' -class TestFinding(typing.NamedTuple): - node_path: str - validator: str - severity: validation.ValidationFindingSeverity - code: str - message: typing.Optional[str] +def register_test(module, file, test_name, validator, filters=None): + if hasattr(module, test_name): + raise ValueError(f'Duplicate test name in {module}: {test_name}') - @staticmethod - def from_dict(d): - return TestFinding(d['node_path'], d['validator'], validation.ValidationFindingSeverity[d['severity']], - d['code'], None if not d['message'] else d['message']) - - @staticmethod - def from_result_and_finding_description(result: validation.ValidationResult, - finding_description: validation.ValidationFindingDescription): - return TestFinding( - result.node.path, - str(result.validator), - finding_description.finding.severity, - finding_description.finding.code, - finding_description.message - ) - - def __repr__(self): - with io.StringIO() as s: - c = csv.writer(s) - - c.writerow([self.node_path, self.validator, str(self.severity), self.code, self.message]) - - return s.getvalue() - - def __str__(self): - return repr(self) - - -def certificate_test_file(file_path): - with open(file_path, 'r', encoding='utf-8') as f: - lines = [l.strip() for l in f.readlines() if l.strip() != ''] - - assert any((_CERT_END_ASCII_ARMOR in line for line in lines)) - - pem_text = '' - - line_num = -1 - for line_num, line in enumerate(lines): - pem_text += line - - if _CERT_END_ASCII_ARMOR in line: - break - - with io.StringIO('\n'.join(lines[line_num + 1:])) as s: - c = csv.DictReader(s) - - expected_findings = set((TestFinding.from_dict(row) for row in c)) - - cert = loader.load_pem_certificate(pem_text, path.basename(file_path)) - cert.decode() - - return cert, expected_findings - - -def run_test(test_file_path, validator, filters=None): - if filters is None: - filters = [] - - cert, expected_findings = certificate_test_file(test_file_path) - - results = validator.validate(cert.root) - results, _ = result_filter.filter_results(filters, results) - - actual_findings = set() - - for result in results: - finding_descriptions = [fd for fd in result.finding_descriptions] - - for finding_description in finding_descriptions: - actual_findings.add(TestFinding.from_result_and_finding_description(result, finding_description)) - - missing_findings = expected_findings - actual_findings - unexpected_findings = actual_findings - expected_findings - - assert not any(missing_findings) and not any(unexpected_findings), ( - f'Missing findings: {missing_findings}\nUnexpected findings: {unexpected_findings}') + setattr(module, test_name, functools.partial( + integration_test_common.run_test, + _CERT_END_ASCII_ARMOR, + loader.load_pem_certificate, + file, + validator, + filters) + ) diff --git a/tests/integration_certificate/test_cabf_serverauth_cert.py b/tests/integration_certificate/test_cabf_serverauth_cert.py index db2401d..d47c88e 100644 --- a/tests/integration_certificate/test_cabf_serverauth_cert.py +++ b/tests/integration_certificate/test_cabf_serverauth_cert.py @@ -1,4 +1,3 @@ -import functools import glob import sys from os import path @@ -6,7 +5,7 @@ from pkilint.cabf import serverauth from pkilint.cabf.serverauth import serverauth_constants from pkilint.pkix import certificate -from tests import integration_certificate +from tests.integration_certificate import register_test this_module = sys.modules[__name__] @@ -27,6 +26,6 @@ file_no_ext, _ = path.splitext(path.basename(file)) - func_name = f'test_{certificate_type}_{file_no_ext}' + test_name = f'test_{certificate_type}_{file_no_ext}' - setattr(this_module, func_name, functools.partial(integration_certificate.run_test, file, validator, filters)) + register_test(this_module, file, test_name, validator, filters) diff --git a/tests/integration_certificate/test_cabf_smime_cert.py b/tests/integration_certificate/test_cabf_smime_cert.py index 7c32398..bc18271 100644 --- a/tests/integration_certificate/test_cabf_smime_cert.py +++ b/tests/integration_certificate/test_cabf_smime_cert.py @@ -3,10 +3,11 @@ import sys from os import path +import tests.integration_test_common from pkilint.cabf import smime from pkilint.cabf.smime import smime_constants from pkilint.pkix import certificate -from tests import integration_certificate +from tests.integration_certificate import register_test this_module = sys.modules[__name__] @@ -29,6 +30,6 @@ file_no_ext, _ = path.splitext(path.basename(file)) - func_name = f'test_{validation_level}-{generation}_{file_no_ext}' + test_name = f'test_{validation_level}-{generation}_{file_no_ext}' - setattr(this_module, func_name, functools.partial(integration_certificate.run_test, file, validator)) + register_test(this_module, file, test_name, validator) diff --git a/tests/integration_certificate/test_etsi_cert.py b/tests/integration_certificate/test_etsi_cert.py index 74b0077..34e4150 100644 --- a/tests/integration_certificate/test_etsi_cert.py +++ b/tests/integration_certificate/test_etsi_cert.py @@ -3,10 +3,11 @@ import sys from os import path +import tests.integration_test_common from pkilint import etsi from pkilint.etsi import etsi_constants from pkilint.pkix import certificate -from tests import integration_certificate +from tests.integration_certificate import register_test this_module = sys.modules[__name__] @@ -27,6 +28,6 @@ file_no_ext, _ = path.splitext(path.basename(file)) - func_name = f'test_{certificate_type}_{file_no_ext}' + test_name = f'test_{certificate_type}_{file_no_ext}' - setattr(this_module, func_name, functools.partial(integration_certificate.run_test, file, validator, filters)) + register_test(this_module, file, test_name, validator, filters) diff --git a/tests/integration_certificate/test_pkix_cert.py b/tests/integration_certificate/test_pkix_cert.py index 625c11c..0e505b2 100644 --- a/tests/integration_certificate/test_pkix_cert.py +++ b/tests/integration_certificate/test_pkix_cert.py @@ -3,8 +3,9 @@ import sys from os import path +import tests.integration_test_common from pkilint.pkix import certificate, name, extension -from tests import integration_certificate +from tests.integration_certificate import register_test cur_dir = path.dirname(__file__) test_dir = path.join(cur_dir, 'pkix') @@ -31,6 +32,6 @@ file_no_ext, _ = path.splitext(path.basename(file)) - func_name = f'test_{file_no_ext}' + test_name = f'test_{file_no_ext}' - setattr(this_module, func_name, functools.partial(integration_certificate.run_test, file, validator)) + register_test(this_module, file, test_name, validator) diff --git a/tests/integration_certificate/tls_br/root_ca/validity_period_no_seconds.crttest b/tests/integration_certificate/tls_br/root_ca/validity_period_no_seconds.crttest index 52b6896..5959b22 100644 --- a/tests/integration_certificate/tls_br/root_ca/validity_period_no_seconds.crttest +++ b/tests/integration_certificate/tls_br/root_ca/validity_period_no_seconds.crttest @@ -35,9 +35,7 @@ certificate,AuthorityKeyIdentifierPresenceValidator,FATAL,base.unhandled_excepti certificate.tbsCertificate.extensions.1.extnValue.keyUsage,CaKeyUsageValidator,NOTICE,cabf.ca_certificate_no_digital_signature_bit, certificate.tbsCertificate.validity.notBefore,RootValidityPeriodValidator,ERROR,pkix.invalid_time_syntax,"notBefore: ""1001010100Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" certificate.tbsCertificate.validity.notAfter.utcTime,UtcTimeCorrectSyntaxValidator,ERROR,pkix.utctime_incorrect_syntax,"""3912312359Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" -certificate.tbsCertificate.validity.notBefore,TimeCorrectEncodingValidator,ERROR,pkix.invalid_time_syntax,"""1001010100Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" certificate.tbsCertificate.extensions,RootExtensionAllowanceValidator,WARNING,cabf.serverauth.root.authority_key_identifier_extension_absent, certificate.tbsCertificate.validity.notBefore.utcTime,UtcTimeCorrectSyntaxValidator,ERROR,pkix.utctime_incorrect_syntax,"""1001010100Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" certificate.tbsCertificate.validity.notBefore,CertificateSaneValidityPeriodValidator,ERROR,pkix.invalid_time_syntax,"notBefore: ""1001010100Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" certificate.tbsCertificate.extensions.2.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified, -certificate.tbsCertificate.validity.notAfter,TimeCorrectEncodingValidator,ERROR,pkix.invalid_time_syntax,"""3912312359Z"" does not match UTCTime regular expression ""^(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})(?P\d{2})Z$""" diff --git a/tests/integration_crl/__init__.py b/tests/integration_crl/__init__.py new file mode 100644 index 0000000..8660529 --- /dev/null +++ b/tests/integration_crl/__init__.py @@ -0,0 +1,22 @@ +import functools +from pathlib import Path + +from pkilint import loader +from tests import integration_test_common + +_FIXTURE_DIR = Path(__file__).parent.resolve() + +_CRL_END_ASCII_ARMOR = '-----END X509 CRL-----' + + +def register_test(module, file, test_name, validator): + if hasattr(module, test_name): + raise ValueError(f'Duplicate test name in {module}: {test_name}') + + setattr(module, test_name, functools.partial( + integration_test_common.run_test, + _CRL_END_ASCII_ARMOR, + loader.load_pem_crl, + file, + validator) + ) diff --git a/tests/integration_crl/cabf/arl/unspecified_reason_code.crltest b/tests/integration_crl/cabf/arl/unspecified_reason_code.crltest new file mode 100644 index 0000000..39251da --- /dev/null +++ b/tests/integration_crl/cabf/arl/unspecified_reason_code.crltest @@ -0,0 +1,15 @@ +-----BEGIN X509 CRL----- +MIIBsDCCATcCAQEwCgYIKoZIzj0EAwMwRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT +GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0 +Fw0yNDA3MjUwOTAwMDBaFw0yNTA3MDEwMDAwMDBaMIGNMC8CEG5Hqc5PRsI94knq +zDiUU3MXDTE5MDkzMDAwMDAwMFowDDAKBgNVHRUEAwoBBTAsAg0B8JxbcAWm3Ibi ++Z7zFw0yMDAxMzEwMDAwMDBaMAwwCgYDVR0VBAMKAQUwLAINAf6lgUR+O/07uBwk +mBcNMjMwNjEzMDAwMDAwWjAMMAoGA1UdFQQDCgEAoC8wLTAKBgNVHRQEAwIBFjAf +BgNVHSMEGDAWgBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBk +AjA5X1pgTcj5bS+WsuSa4wEph63mswxZzAABv48n5Eux2mivldbo/H5r5whnYuHB +Y70CMCAZb2XUaCri3uaxtNbLp1nwCD/G7eRY0sdhpqVUAeqFNQ8MZPT+mMZo1B4C +c762xg== +-----END X509 CRL----- + +node_path,validator,severity,code,message +certificateList.tbsCertList.revokedCertificates.2.crlEntryExtensions.0.extnValue.cRLReason,CabfCrlReasonCodeAllowlistValidator,ERROR,cabf.crl_prohibited_reason_code,"Prohibited reason code ""unspecified""" diff --git a/tests/integration_crl/pkix/crl/bad_idp_uri.crltest b/tests/integration_crl/pkix/crl/bad_idp_uri.crltest new file mode 100644 index 0000000..dec0f11 --- /dev/null +++ b/tests/integration_crl/pkix/crl/bad_idp_uri.crltest @@ -0,0 +1,15 @@ +-----BEGIN X509 CRL----- +MIIBvDCBpQIBATANBgkqhkiG9w0BAQsFADAiMQswCQYDVQQGEwJYWDETMBEGA1UE +CgwKQ1JMcyAnciBVcxcNMjQwMzI1MTg0NzAwWhcNMjQwNDAxMTg0NzAwWqBPME0w +CgYDVR0UBAMCAQEwHwYDVR0jBBgwFoAU/NE0t8uklbG2WeoLBWIe6JqPtDowHgYD +VR0cAQH/BBQwEqANoAuGCWxvY2FsaG9zdIQB/zANBgkqhkiG9w0BAQsFAAOCAQEA +DfKA0r1rINyb1CeDJF73M2hW+9lR9Cf1S+T1dnAUpjH2lQSp1LDa9SHMU9OihfID +aw06SqifX3rTtvdLMGX3D7fbBFVAVUFKXZUHN7cmtY2FphZAN7ZKah+awQkGtwgZ +tQoPHGxQbcFoFq+9DbFb3w+nrYwBD/WXvjEl5xldKcPRHjldOx50g/zxkMKgomkp ++Q3SA1aB/HhUhpoDS8Pm+EsFI5s/D6rGcLx8Sb5XldQ/HI+83UeMsIETIxHyWfAa +fk3buuBr9HaA0tIhCSLISx2JHFtaJKav7T5vJCgGw63qWq3jzUMIUnFXA4ki+93h +ASD6JIBt0IaIYrnD+THhdQ== +-----END X509 CRL----- + +node_path,validator,severity,code,message +certificateList.tbsCertList.crlExtensions.2.extnValue.issuingDistributionPoint.distributionPoint.fullName.0.uniformResourceIdentifier,GeneralNameUriSyntaxValidator,ERROR,pkix.invalid_uri_syntax,"Invalid URI syntax: ""localhost""" diff --git a/tests/integration_crl/pkix/crl/negative_validity_period.crltest b/tests/integration_crl/pkix/crl/negative_validity_period.crltest new file mode 100644 index 0000000..780b17d --- /dev/null +++ b/tests/integration_crl/pkix/crl/negative_validity_period.crltest @@ -0,0 +1,14 @@ +-----BEGIN X509 CRL----- +MIIBnDCBhQIBATANBgkqhkiG9w0BAQsFADAiMQswCQYDVQQGEwJYWDETMBEGA1UE +CgwKQ1JMcyAnciBVcxcNMjQwMzI1MTg0NzAwWhcNMjMwNDAxMTg0NzAwWqAvMC0w +CgYDVR0UBAMCAQEwHwYDVR0jBBgwFoAU/NE0t8uklbG2WeoLBWIe6JqPtDowDQYJ +KoZIhvcNAQELBQADggEBAA3ygNK9ayDcm9QngyRe9zNoVvvZUfQn9Uvk9XZwFKYx +9pUEqdSw2vUhzFPTooXyA2sNOkqon19607b3SzBl9w+32wRVQFVBSl2VBze3JrWN +haYWQDe2SmofmsEJBrcIGbUKDxxsUG3BaBavvQ2xW98Pp62MAQ/1l74xJecZXSnD +0R45XTsedIP88ZDCoKJpKfkN0gNWgfx4VIaaA0vD5vhLBSObPw+qxnC8fEm+V5XU +PxyPvN1HjLCBEyMR8lnwGn5N27rga/R2gNLSIQkiyEsdiRxbWiSmr+0+byQoBsOt +6lqt481DCFJxVwOJIvvd4QEg+iSAbdCGiGK5w/kx4XU= +-----END X509 CRL----- + +node_path,validator,severity,code,message +certificateList.tbsCertList.thisUpdate,CrlSaneValidityPeriodValidator,ERROR,pkix.crl_negative_validity_period,"Start of validity period ""2024-03-25 18:47:00+00:00"" is greater than end of validity period ""2023-04-01 18:47:00+00:00""" diff --git a/tests/integration_crl/test_cabf_arl.py b/tests/integration_crl/test_cabf_arl.py new file mode 100644 index 0000000..c2a0354 --- /dev/null +++ b/tests/integration_crl/test_cabf_arl.py @@ -0,0 +1,43 @@ +import glob +import sys +from os import path + +from pkilint import pkix +from pkilint.cabf import cabf_crl +from pkilint.pkix import name, extension, crl +from tests.integration_crl import register_test + +cur_dir = path.dirname(__file__) +test_dir = path.join(cur_dir, 'cabf', 'arl') +this_module = sys.modules[__name__] + +files = glob.glob(path.join(test_dir, '*.crltest')) + + +for file in files: + validator = crl.create_pkix_crl_validator_container( + [ + pkix.create_attribute_decoder(name.ATTRIBUTE_TYPE_MAPPINGS), + pkix.create_extension_decoder(extension.EXTENSION_MAPPINGS), + ], + [ + crl.create_issuer_validator_container( + [] + ), + crl.create_validity_validator_container( + [cabf_crl.create_validity_period_validator(crl.CertificateRevocationListType.ARL)] + ), + crl.create_extensions_validator_container( + [] + ), + ] + + [ + cabf_crl.CabfCrlReasonCodeAllowlistValidator(crl.CertificateRevocationListType.ARL) + ] + ) + + file_no_ext, _ = path.splitext(path.basename(file)) + + test_name = f'test_{file_no_ext}' + + register_test(this_module, file, test_name, validator) diff --git a/tests/integration_crl/test_pkix_crl.py b/tests/integration_crl/test_pkix_crl.py new file mode 100644 index 0000000..db4f3a7 --- /dev/null +++ b/tests/integration_crl/test_pkix_crl.py @@ -0,0 +1,39 @@ +import glob +import sys +from os import path + +from pkilint import pkix +from pkilint.pkix import name, extension, crl +from tests.integration_crl import register_test + +cur_dir = path.dirname(__file__) +test_dir = path.join(cur_dir, 'pkix', 'crl') +this_module = sys.modules[__name__] + +files = glob.glob(path.join(test_dir, '*.crltest')) + + +for file in files: + validator = crl.create_pkix_crl_validator_container( + [ + pkix.create_attribute_decoder(name.ATTRIBUTE_TYPE_MAPPINGS), + pkix.create_extension_decoder(extension.EXTENSION_MAPPINGS), + ], + [ + crl.create_issuer_validator_container( + [] + ), + crl.create_validity_validator_container( + [] + ), + crl.create_extensions_validator_container( + [] + ), + ] + ) + + file_no_ext, _ = path.splitext(path.basename(file)) + + test_name = f'test_{file_no_ext}' + + register_test(this_module, file, test_name, validator) diff --git a/tests/integration_test_common.py b/tests/integration_test_common.py new file mode 100644 index 0000000..1d20e65 --- /dev/null +++ b/tests/integration_test_common.py @@ -0,0 +1,91 @@ +import csv +import io +import typing +from os import path + +from pkilint import validation, finding_filter as result_filter + + +class TestFinding(typing.NamedTuple): + node_path: str + validator: str + severity: validation.ValidationFindingSeverity + code: str + message: typing.Optional[str] + + @staticmethod + def from_dict(d): + return TestFinding(d['node_path'], d['validator'], validation.ValidationFindingSeverity[d['severity']], + d['code'], None if not d['message'] else d['message']) + + @staticmethod + def from_result_and_finding_description(result: validation.ValidationResult, + finding_description: validation.ValidationFindingDescription): + return TestFinding( + result.node.path, + str(result.validator), + finding_description.finding.severity, + finding_description.finding.code, + finding_description.message + ) + + def __repr__(self): + with io.StringIO() as s: + c = csv.writer(s) + + c.writerow([self.node_path, self.validator, str(self.severity), self.code, self.message]) + + return s.getvalue() + + def __str__(self): + return repr(self) + + +def load_test_file(ascii_armor_end, loader_func, file_path): + with open(file_path, 'r', encoding='utf-8') as f: + lines = [l.strip() for l in f.readlines() if l.strip() != ''] + + assert any((ascii_armor_end in line for line in lines)) + + pem_text = '' + + line_num = -1 + for line_num, line in enumerate(lines): + pem_text += line + + if ascii_armor_end in line: + break + + with io.StringIO('\n'.join(lines[line_num + 1:])) as s: + c = csv.DictReader(s) + + expected_findings = set((TestFinding.from_dict(row) for row in c)) + + doc = loader_func(pem_text, path.basename(file_path)) + doc.decode() + + return doc, expected_findings + + +def run_test(ascii_armor_end, loader_func, test_file_path, validator, filters=None): + if filters is None: + filters = [] + + cert, expected_findings = load_test_file(ascii_armor_end, loader_func, test_file_path) + + results = validator.validate(cert.root) + results, _ = result_filter.filter_results(filters, results) + + actual_findings = set() + + for result in results: + finding_descriptions = [fd for fd in result.finding_descriptions] + + for finding_description in finding_descriptions: + actual_findings.add(TestFinding.from_result_and_finding_description(result, finding_description)) + + missing_findings = expected_findings - actual_findings + unexpected_findings = actual_findings - expected_findings + + assert not any(missing_findings) and not any(unexpected_findings), ( + f'Missing findings: {missing_findings}\nUnexpected findings: {unexpected_findings}')