diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dbc38a..3a98455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project from version 0.9.3 onwards are documented in this file. +## 0.11.1 - 2024-07-02 + +### New features/enhancements + +- Add support for PEM-encoded OCSP responses (#86) +- Add validator to verify that the PSD2 policy OID is only asserted in PSD2 certificates (#87) +- Add validator to flag insignificant attribute values (#84) + +### Fixes + +- Perform case-sensitive match for ISO 3166-1 country codes (#83) + ## 0.11.0 - 2024-06-14 ### New features/enhancements diff --git a/VERSION.txt b/VERSION.txt index 142464b..027934e 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.11.0 \ No newline at end of file +0.11.1 \ No newline at end of file diff --git a/pkilint/bin/convert_cert.py b/pkilint/bin/convert_cert.py deleted file mode 100644 index f04d6c6..0000000 --- a/pkilint/bin/convert_cert.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -import argparse - -from cryptography import x509 -from cryptography.hazmat.primitives.serialization import Encoding - - -def _convert_cert(f): - content = f.read() - - if content.startswith(b'\x30'): - cert = x509.load_der_x509_certificate(content) - else: - cert = x509.load_pem_x509_certificate(content) - - return cert.public_bytes(Encoding.PEM).decode('us-ascii') - - -parser = argparse.ArgumentParser() -parser.add_argument('file', type=argparse.FileType('rb')) - -args = parser.parse_args() - -pem = _convert_cert(args.file) - -print(f'pem = """{pem}"""') diff --git a/pkilint/bin/lint_cabf_serverauth_cert.py b/pkilint/bin/lint_cabf_serverauth_cert.py index f97fcdb..e851ae3 100644 --- a/pkilint/bin/lint_cabf_serverauth_cert.py +++ b/pkilint/bin/lint_cabf_serverauth_cert.py @@ -73,7 +73,7 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate(args.file, args.file.name) + cert = loader.load_certificate_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_cabf_smime_cert.py b/pkilint/bin/lint_cabf_smime_cert.py index 4e7addb..37721ef 100644 --- a/pkilint/bin/lint_cabf_smime_cert.py +++ b/pkilint/bin/lint_cabf_smime_cert.py @@ -117,7 +117,7 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate(args.file, args.file.name) + cert = loader.load_certificate_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_crl.py b/pkilint/bin/lint_crl.py index 09edfc1..5ef107f 100644 --- a/pkilint/bin/lint_crl.py +++ b/pkilint/bin/lint_crl.py @@ -80,7 +80,7 @@ def main(cli_args=None) -> int: return 0 else: try: - crl_doc = loader.load_crl(args.file, args.file.name) + crl_doc = loader.load_crl_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load CRL: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_etsi_cert.py b/pkilint/bin/lint_etsi_cert.py index 3fa6ec3..9c2ce2e 100644 --- a/pkilint/bin/lint_etsi_cert.py +++ b/pkilint/bin/lint_etsi_cert.py @@ -71,7 +71,7 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate(args.file, args.file.name) + cert = loader.load_certificate_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_ocsp_response.py b/pkilint/bin/lint_ocsp_response.py index 066be93..c4f0890 100644 --- a/pkilint/bin/lint_ocsp_response.py +++ b/pkilint/bin/lint_ocsp_response.py @@ -38,7 +38,7 @@ def main(cli_args=None) -> int: return 0 else: try: - ocsp_response = loader.load_ocsp_response(args.file, args.file.name) + ocsp_response = loader.load_ocsp_response_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load OCSP response: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_pkix_cert.py b/pkilint/bin/lint_pkix_cert.py index b9b54d1..21aea1b 100644 --- a/pkilint/bin/lint_pkix_cert.py +++ b/pkilint/bin/lint_pkix_cert.py @@ -45,7 +45,7 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate(args.file, args.file.name) + cert = loader.load_certificate_file(args.file, args.file.name) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 diff --git a/pkilint/bin/lint_pkix_signer_signee_cert_chain.py b/pkilint/bin/lint_pkix_signer_signee_cert_chain.py index 61f36d8..a84bf3a 100644 --- a/pkilint/bin/lint_pkix_signer_signee_cert_chain.py +++ b/pkilint/bin/lint_pkix_signer_signee_cert_chain.py @@ -94,9 +94,9 @@ def main(cli_args=None) -> int: doc_collection = {} try: - issuer = loader.load_certificate(args.issuer, args.issuer.name, 'issuer', - doc_collection - ) + issuer = loader.load_certificate_file( + args.issuer, args.issuer.name, 'issuer', doc_collection + ) except ValueError as e: print(f'Failed to load issuer certificate: {e}', file=sys.stderr) return 1 @@ -104,9 +104,9 @@ def main(cli_args=None) -> int: doc_collection['issuer'] = issuer try: - subject = loader.load_certificate(args.subject, args.subject.name, 'subject', - doc_collection - ) + subject = loader.load_certificate_file( + args.subject, args.subject.name, 'subject', doc_collection + ) except ValueError as e: print(f'Failed to load subject certificate: {e}', file=sys.stderr) return 1 diff --git a/pkilint/cabf/cabf_name.py b/pkilint/cabf/cabf_name.py index 3d5fc9a..7bd7455 100644 --- a/pkilint/cabf/cabf_name.py +++ b/pkilint/cabf/cabf_name.py @@ -1,12 +1,13 @@ import typing +import unicodedata from iso3166 import countries_by_alpha2 from pyasn1_alt_modules import rfc5280 from pkilint import validation, document from pkilint.common import organization_id from pkilint.common.organization_id import ParsedOrganizationIdentifier -from pkilint.itu import x520_name +from pkilint.itu import x520_name, asn1_util from pkilint.pkix import Rfc2119Word @@ -19,7 +20,7 @@ def __init__(self, type_oid, value_path, checked_validation): ) def validate_with_value(self, node, value_node): - country_code = str(value_node.pdu).upper() + country_code = str(value_node.pdu) if country_code == 'XX': return @@ -196,8 +197,36 @@ def validate(self, node): 'cabf.internal_ip_address' ) - VALIDATION_INTERNAL_DOMAIN_NAME = validation.ValidationFinding( validation.ValidationFindingSeverity.ERROR, 'cabf.internal_domain_name' ) + + +class SignificantAttributeValueValidator(validation.Validator): + VALIDATION_INSIGNIFICANT_ATTRIUBTE_VALUE_PRESENT = validation.ValidationFinding( + validation.ValidationFindingSeverity.ERROR, + 'cabf.insignificant_attribute_value_present' + ) + + # https://www.unicode.org/reports/tr44/#General_Category_Values + # TODO: any letter, number, or symbol is significant. revisit this to restrict S to only Sc (currency symbol)? + _SIGNIFICANT_MAJOR_CLASSES = {'L', 'N', 'S', } + + def __init__(self): + super().__init__( + validations=[self.VALIDATION_INSIGNIFICANT_ATTRIUBTE_VALUE_PRESENT], + pdu_class=rfc5280.AttributeTypeAndValue + ) + + def validate(self, node): + value = asn1_util.get_string_value_from_attribute_node(node) + + if value is None: + return + + if not any(unicodedata.category(c)[0] in self._SIGNIFICANT_MAJOR_CLASSES for c in value): + raise validation.ValidationFindingEncountered( + self.VALIDATION_INSIGNIFICANT_ATTRIUBTE_VALUE_PRESENT, + f'Insignificant attribute value: "{value}"' + ) diff --git a/pkilint/cabf/serverauth/__init__.py b/pkilint/cabf/serverauth/__init__.py index 78547f5..c62ed07 100644 --- a/pkilint/cabf/serverauth/__init__.py +++ b/pkilint/cabf/serverauth/__init__.py @@ -152,6 +152,7 @@ def create_subject_name_validators() -> List[validation.Validator]: serverauth_name.ValidBusinessCategoryValidator(), cabf_name.CabfOrganizationIdentifierAttributeValidator(), serverauth_name.ServerauthRelativeDistinguishedNameContainsOneElementValidator(), + cabf_name.SignificantAttributeValueValidator(), ] diff --git a/pkilint/cabf/serverauth/finding_metadata.csv b/pkilint/cabf/serverauth/finding_metadata.csv index 049db01..34ec6b6 100644 --- a/pkilint/cabf/serverauth/finding_metadata.csv +++ b/pkilint/cabf/serverauth/finding_metadata.csv @@ -19,6 +19,7 @@ ERROR,cabf.ev_guidelines.organization_name_attribute_absent,Validates that the c ERROR,cabf.ev_guidelines.prohibited_san_type,Validates that the types of GeneralNames included in the SAN extension conform to EVG 9.8.1. ERROR,cabf.ev_guidelines.serial_number_attribute_absent,Validates that the content of the subject conforms to EVG 9.2.: A required element is absent ERROR,cabf.ev_guidelines.unknown_attribute_present,Validates that the content of the subject conforms to EVG 9.2.: A prohibited element is present +cabf.insignificant_attribute_value_present,"Subject attributes SHALL NOT contain only metadata such as '.', '-', and ' ' (i.e., space) characters, and/or any other indication that the value is absent, incomplete, or not applicable." ERROR,cabf.internal_domain_name,An Internal Domain has been specified. ERROR,cabf.internal_ip_address,A Reserved IP Address has been specified. ERROR,cabf.invalid_country_code,A country code which does not appear on the ISO 3166-1 list has been specified. diff --git a/pkilint/cabf/smime/finding_metadata.csv b/pkilint/cabf/smime/finding_metadata.csv index c9e5a17..89f32fb 100644 --- a/pkilint/cabf/smime/finding_metadata.csv +++ b/pkilint/cabf/smime/finding_metadata.csv @@ -6,6 +6,7 @@ ERROR,cabf.aia_ca_issuers_has_no_http_uri,SMBR 7.1.2.3 (c),"Legacy: ""When provi ERROR,cabf.aia_ocsp_has_no_http_uri,SMBR 7.1.2.3 (c),"Legacy: ""When provided, at least one accessMethod SHALL have the URI scheme HTTP"". MP and strict: ""When provided, every accessMethod SHALL have the URI scheme HTTP""" ERROR,cabf.authority_key_identifier_has_issuer_cert,SMBR 7.1.2.3 (g),"""authorityCertIssuer and authorityCertSerialNumber fields SHALL NOT be present.""" ERROR,cabf.crldp_extension_missing,SMBR 7.1.2.3 (b),"""SHALL be present""" +ERROR,cabf.insignificant_attribute_value_present,SMBR 7.1.4.2,"Subject attributes SHALL NOT contain only metadata such as '.', '-', and ' ' (i.e., space) characters, and/or any other indication that the value is absent, incomplete, or not applicable." ERROR,cabf.internal_domain_name,,The use of an internal domain name (whose superior domain labels do not appear on the PSL) ERROR,cabf.invalid_country_code,,The use of a country code that does not appear on ISO 3166. ERROR,cabf.invalid_organization_identifier_country,SMBR 7.1.4.2.2 (d),The use of a country code that is not allowed in the organizationIdentifier attribute diff --git a/pkilint/cabf/smime/smime_name.py b/pkilint/cabf/smime/smime_name.py index 0fdb89e..8fb7c90 100644 --- a/pkilint/cabf/smime/smime_name.py +++ b/pkilint/cabf/smime/smime_name.py @@ -1,5 +1,4 @@ import validators -from pyasn1.type import char from pyasn1_alt_modules import rfc5280, rfc8398 from pkilint import validation, pkix, oid @@ -8,8 +7,8 @@ from pkilint.cabf.smime.smime_constants import Generation, ValidationLevel from pkilint.common import organization_id from pkilint.common.organization_id import OrganizationIdentifierLeiValidator -from pkilint.itu import x520_name -from pkilint.pkix import certificate, name, Rfc2119Word +from pkilint.itu import x520_name, asn1_util +from pkilint.pkix import certificate, name, Rfc2119Word, general_name SHALL = pkix.Rfc2119Word.SHALL SHALL_NOT = pkix.Rfc2119Word.SHALL_NOT @@ -288,6 +287,7 @@ def create_subscriber_certificate_subject_validator_container( OrganizationIdentifierLeiValidator(), OrganizationIdentifierCountryNameConsistentValidator(), cabf_name.RelativeDistinguishedNameContainsOneElementValidator(), + cabf_name.SignificantAttributeValueValidator(), ] return certificate.create_subject_validator_container( @@ -322,27 +322,15 @@ def __init__(self): def validate(self, node): oid = node.children['type'].pdu - value = node.children['value'] - while True: - if len(value.children) != 1: - value = None - break - else: - _, value = value.child + value_str = asn1_util.get_string_value_from_attribute_node(node) - if len(value.children) == 0: - if isinstance(value.pdu, char.AbstractCharacterString): - break - - if value is None: + if value_str is None: raise validation.ValidationFindingEncountered( self.VALIDATION_UNPARSED_ATTRIBUTE, f'Unparsed attribute {str(oid)} encountered' ) - value_str = str(value.pdu) - if bool(validators.email(value_str)): san_email_addresses = get_email_addresses_from_san(node.document) @@ -375,20 +363,9 @@ def __init__(self, validation_level, generation): @staticmethod def _is_value_in_dirstring_atvs(atvs, expected_value_node): - for atv in atvs: - try: - # get the value contained within the DirectoryString-encoded ATV value - _, atv_dirstring_value_node = atv.children['value'].child - _, value = atv_dirstring_value_node.child - except ValueError: - # skip unparsed field - - continue + expected_value_str = str(expected_value_node.pdu) - if str(value.pdu) == str(expected_value_node.pdu): - return True - - return False + return any(expected_value_str == asn1_util.get_string_value_from_attribute_node(a) for a in atvs) def validate(self, node): try: @@ -446,17 +423,11 @@ def validate(self, node): country_name_value = str(node.pdu) for atv, _ in node.document.get_subject_attributes_by_type(x520_name.id_at_organizationIdentifier): - attr_value_node = atv.navigate('value') + x520_value_str = asn1_util.get_string_value_from_attribute_node(atv) - try: - _, x520_dirstring_value_node = attr_value_node.child - except ValueError: + if x520_value_str is None: continue - _, x520_value_node = x520_dirstring_value_node.child - - x520_value_str = str(x520_value_node.pdu) - try: parsed_org_id = organization_id.parse_organization_identifier(x520_value_str) except ValueError: @@ -465,10 +436,10 @@ def validate(self, node): orgid_country_name = parsed_org_id.country # skip this orgId attribute if it contains the global scheme identifier - if orgid_country_name.casefold() == organization_id.COUNTRY_CODE_GLOBAL_SCHEME.casefold(): + if orgid_country_name == organization_id.COUNTRY_CODE_GLOBAL_SCHEME: continue - if orgid_country_name.casefold() != country_name_value.casefold(): + if orgid_country_name != country_name_value: raise validation.ValidationFindingEncountered( self.VALIDATION_ORGID_COUNTRYNAME_INCONSISTENT, f'CountryName attribute value: "{country_name_value}", ' @@ -488,9 +459,12 @@ def get_email_addresses_from_san(cert_document): for gn in san_ext.navigate('extnValue.subjectAltName').children.values(): name, value = gn.child - if name == 'rfc822Name': + if name == general_name.GeneralNameTypeName.RFC822_NAME: email_addresses.append(value.pdu) - elif name == 'otherName' and value.navigate('type-id').pdu == rfc8398.id_on_SmtpUTF8Mailbox: + elif ( + name == general_name.GeneralNameTypeName.OTHER_NAME and + value.navigate('type-id').pdu == rfc8398.id_on_SmtpUTF8Mailbox + ): email_addresses.append(value.navigate('value').child[1].pdu) return email_addresses diff --git a/pkilint/etsi/__init__.py b/pkilint/etsi/__init__.py index 142c04e..3b7c227 100644 --- a/pkilint/etsi/__init__.py +++ b/pkilint/etsi/__init__.py @@ -209,6 +209,7 @@ def create_validators(certificate_type: CertificateType, extension_validators = [ en_319_412_2.QualifiedCertificatePoliciesValidator(certificate_type), en_319_412_5.QcStatementsExtensionCriticalityValidator(), + ts_119_495.Psd2CertificatePolicyOidPresenceValidator(certificate_type), qc_statements_validator_container, ] diff --git a/pkilint/etsi/en_319_411_1.py b/pkilint/etsi/en_319_411_1.py index 0acfd09..b1760ca 100644 --- a/pkilint/etsi/en_319_411_1.py +++ b/pkilint/etsi/en_319_411_1.py @@ -20,6 +20,7 @@ class CertificatePoliciesValidator(validation.Validator): 'etsi.en_319_411_1.gen-6.3.3-12.prohibited_reserved_policy_oid_present', ) + # mapping of certificate types to ETSI policy OIDs _CERTIFICATE_TYPE_SET_TO_POLICY_OID_MAPPINGS = [ (etsi_constants.CABF_EV_CERTIFICATE_TYPES, en_319_411_1.id_evcp), (etsi_constants.CABF_DV_CERTIFICATE_TYPES, en_319_411_1.id_dvcp), @@ -41,8 +42,10 @@ def __init__(self, certificate_type): ) def validate(self, node): + # extract ETSI reserved policy OIDs from certificate policy OIDs etsi_policy_oids = en_319_411_1.POLICY_OIDS & node.document.policy_oids + # if multiple ETSI policy OIDs are present, then report if len(etsi_policy_oids) > 1: oids = oid.format_oids(etsi_policy_oids) @@ -51,6 +54,7 @@ def validate(self, node): f'Multiple reserved certificate policy OIDs present: {oids}' ) + # if there is a mismatch between the certificate type and reserved ETSI policy OID, then report if etsi_policy_oids and self._expected_policy_oid not in etsi_policy_oids: prohibited_oid = next(iter(etsi_policy_oids)) diff --git a/pkilint/etsi/en_319_412_3.py b/pkilint/etsi/en_319_412_3.py index 9953c7b..5de4420 100644 --- a/pkilint/etsi/en_319_412_3.py +++ b/pkilint/etsi/en_319_412_3.py @@ -4,7 +4,7 @@ from pkilint import validation from pkilint.common import organization_id from pkilint.etsi import etsi_shared -from pkilint.itu import x520_name +from pkilint.itu import x520_name, asn1_util from pkilint.pkix import Rfc2119Word, name _REQUIRED_ATTRIBUTES = { @@ -70,16 +70,6 @@ def __init__(self): pdu_class=rfc5280.Name ) - @classmethod - def _get_dirstring_attribute_value(cls, node): - try: - _, value_node = node.children['value'].child - _, decoded_value_node = value_node.child - - return str(decoded_value_node.pdu) - except ValueError: - return None - def validate(self, node): # only get the first instance of the attributes orgname_attr_and_idx = next( @@ -93,8 +83,8 @@ def validate(self, node): orgname_attr, _ = orgname_attr_and_idx orgid_attr, _ = orgid_attr_and_idx - orgname = self._get_dirstring_attribute_value(orgname_attr) - orgid = self._get_dirstring_attribute_value(orgid_attr) + orgname = asn1_util.get_string_value_from_attribute_node(orgname_attr) + orgid = asn1_util.get_string_value_from_attribute_node(orgid_attr) # if any of the attributes were not decoded, then return early if orgname is None or orgid is None: diff --git a/pkilint/etsi/etsi_constants.py b/pkilint/etsi/etsi_constants.py index 00326f6..7ba3a7c 100644 --- a/pkilint/etsi/etsi_constants.py +++ b/pkilint/etsi/etsi_constants.py @@ -141,6 +141,8 @@ def from_option_str(value): CertificateType.QEVCP_W_PSD2_EIDAS_FINAL_CERTIFICATE, } | QEVCP_W_PSD2_EIDAS_NON_BROWSER_CERTIFICATE_TYPES +PSD2_EIDAS_CERTIFICATE_TYPES = QEVCP_W_PSD2_EIDAS_CERTIFICATE_TYPES + QEVCP_W_EIDAS_CERTIFICATE_TYPES = { CertificateType.QEVCP_W_EIDAS_PRE_CERTIFICATE, CertificateType.QEVCP_W_EIDAS_FINAL_CERTIFICATE diff --git a/pkilint/etsi/ts_119_495.py b/pkilint/etsi/ts_119_495.py index 3464d3e..33ed2aa 100644 --- a/pkilint/etsi/ts_119_495.py +++ b/pkilint/etsi/ts_119_495.py @@ -1,10 +1,12 @@ import re -from pkilint import validation -from pkilint.etsi.asn1 import ts_119_495 as ts_119_495_asn1 -from pyasn1_alt_modules import rfc3739 from iso3166 import countries_by_alpha2 +from pyasn1_alt_modules import rfc3739, rfc5280 + import pkilint.oid +from pkilint import validation +from pkilint.etsi import etsi_constants +from pkilint.etsi.asn1 import ts_119_495 as ts_119_495_asn1 from pkilint.itu import x520_name @@ -192,3 +194,32 @@ def validate(self, node): self.VALIDATION_INVALID_PSD_ORGANIZATION_ID_FORMAT, f'Invalid PSD organization identifier format: "{value_str}"' ) + + +class Psd2CertificatePolicyOidPresenceValidator(validation.Validator): + """ + OVR-6.1-3: TSPs issuing certificates for EU PSD2 may use the following policy identifier to augment the policy + requirements associated with policy identifier QEVCP-w or QNCP-w as specified in ETSI EN 319 411-2 [5] giving + precedence to the requirements defined in the present document. + """ + VALIDATION_PROHIBITED_PSD2_POLICY_OID_PRESENT = validation.ValidationFinding( + validation.ValidationFindingSeverity.ERROR, + 'etsi.ts_119_495.ovr-6.1-3.prohibited_psd2_policy_oid_present' + ) + + def __init__(self, certificate_type): + super().__init__( + validations=[self.VALIDATION_PROHIBITED_PSD2_POLICY_OID_PRESENT], pdu_class=rfc5280.CertificatePolicies + ) + + self._certificate_type = certificate_type + + def validate(self, node): + if ( + ts_119_495_asn1.qcp_web_psd2 in node.document.policy_oids and + self._certificate_type not in etsi_constants.PSD2_EIDAS_CERTIFICATE_TYPES + ): + raise validation.ValidationFindingEncountered( + self.VALIDATION_PROHIBITED_PSD2_POLICY_OID_PRESENT, + f'Certificate type is "{self._certificate_type}" but PSD2 policy identifier is present' + ) diff --git a/pkilint/itu/asn1_util.py b/pkilint/itu/asn1_util.py new file mode 100644 index 0000000..886b1cd --- /dev/null +++ b/pkilint/itu/asn1_util.py @@ -0,0 +1,21 @@ +from typing import Optional + +from pyasn1.type import univ + +from pkilint import document + + +def get_string_value_from_attribute_node(node: document.PDUNode) -> Optional[str]: + node = node.children['value'] + + try: + _, node = node.child + except ValueError: + # attribute value has not been decoded + return None + + # handle DirectoryString CHOICE + if isinstance(node.pdu, univ.Choice): + _, node = node.child + + return str(node.pdu) diff --git a/pkilint/loader.py b/pkilint/loader.py index 4065925..4105dc3 100644 --- a/pkilint/loader.py +++ b/pkilint/loader.py @@ -1,144 +1,128 @@ import base64 -import functools import re -import sys from pkilint.pkix.certificate import RFC5280Certificate from pkilint.pkix.crl import RFC5280CertificateList from pkilint.pkix.ocsp import RFC6960OCSPResponse -def _create_ascii_armor(document_kind: str): - document_kind = document_kind.upper() +class DocumentLoader: + def __init__(self, document_cls, document_pem_label: str): + self._document_cls = document_cls + self._document_pem_label = document_pem_label.upper() - return f'-----BEGIN {document_kind}-----', f'-----END {document_kind}-----' + self._pem_re = self._create_pem_re() + def _create_pem_re(self) -> re.Pattern: + ascii_armor_start = f'-----BEGIN {self._document_pem_label}-----' + ascii_armor_end = f'-----END {self._document_pem_label}-----' -def _create_pem_re(ascii_armor=('', '')) -> re.Pattern: - return re.compile(f'^\\s*{ascii_armor[0]}(?P.+){ascii_armor[1]}\\s*$', re.DOTALL) + return re.compile(f'^\\s*{ascii_armor_start}(?P.+){ascii_armor_end}\\s*$', re.DOTALL) + def load_der_document(self, substrate: bytes, document_name: str = None, substrate_source: str = None, parent=None): + if not substrate.startswith(b'\x30'): + raise ValueError('Substrate is not DER-encoded') -_CERTIFICATE_PEM_REGEX = _create_pem_re(_create_ascii_armor('CERTIFICATE')) -_CRL_PEM_REGEX = _create_pem_re(_create_ascii_armor('X509 CRL')) -_GENERIC_BASE64_REGEX = _create_pem_re() + doc = self._document_cls(substrate_source, substrate, document_name, parent) + doc.decode() -_DOCUMENT_CLS_TO_PEM_REGEX = { - RFC5280Certificate: _CERTIFICATE_PEM_REGEX, - RFC5280CertificateList: _CRL_PEM_REGEX, -} + return doc + def load_der_file(self, f, document_name: str = None, substrate_source: str = None, parent=None): + return self.load_der_document(f.read(), document_name, substrate_source, parent) -def _convert_pem_str_to_der(regex: re.Pattern, pem_text: str) -> bytes: - m = regex.match(pem_text) + def load_b64_document(self, substrate: str, document_name: str = None, substrate_source: str = None, parent=None): + der = base64.b64decode(substrate) - if m is None: - raise ValueError('Invalid PEM text') + return self.load_der_document(der, document_name, substrate_source, parent) - b64_text = m.group('pem') + def load_b64_file(self, f, document_name: str = None, substrate_source: str = None, parent=None): + data = f.read() - return base64.b64decode(b64_text) + if isinstance(data, bytes): + data = data.decode('us-ascii') + return self.load_b64_document(data, document_name, substrate_source, parent) -def _convert_pem_bytes_to_der(regex: re.Pattern, pem_text: bytes) -> bytes: - return _convert_pem_str_to_der(regex, pem_text.decode()) + def load_pem_document(self, substrate: str, document_name: str = None, substrate_source: str = None, parent=None): + m = self._pem_re.match(substrate) + if m is None: + raise ValueError('Invalid PEM text') -def _load_der_document(document_cls, substrate: bytes, document_name: str = None, - substrate_source: str = None, parent=None): - if not substrate.startswith(b'\x30'): - raise ValueError('Substrate is not DER-encoded') + b64_text = m.group('pem') - doc = document_cls(substrate_source, substrate, document_name, parent) - doc.decode() + return self.load_b64_document(b64_text, document_name, substrate_source, parent) - return doc + def load_pem_file(self, f, document_name: str = None, substrate_source: str = None, parent=None): + data = f.read() + if isinstance(data, bytes): + data = data.decode('us-ascii') -def _load_pem_document(document_cls, substrate: str, document_name: str = None, - substrate_source: str = None, parent=None): - regex = _DOCUMENT_CLS_TO_PEM_REGEX.get(document_cls, _GENERIC_BASE64_REGEX) + return self.load_pem_document(data, document_name, substrate_source, parent) - der = _convert_pem_str_to_der(regex, substrate) + @classmethod + def _is_ascii_armor_start_present(cls, substrate: str): + first_significant_char = next((c for c in substrate if not c.isspace()), None) - return _load_der_document(document_cls, der, document_name, substrate_source, parent) + return first_significant_char == '-' + def load_document(self, substrate, document_name: str = None, substrate_source: str = None, parent=None): + if isinstance(substrate, bytes): + try: + return self.load_der_document(substrate, document_name, substrate_source, parent) + except ValueError: + substrate = substrate.decode('us-ascii') -def _load_pem_file(document_cls, f, document_name: str = None, substrate_source: str = None, parent=None): - data = f.read() - - regex = _DOCUMENT_CLS_TO_PEM_REGEX.get(document_cls, _GENERIC_BASE64_REGEX) - - if isinstance(data, bytes): - der = _convert_pem_bytes_to_der(regex, data) - else: - der = _convert_pem_str_to_der(regex, data) - - return _load_der_document(document_cls, der, document_name, substrate_source, parent) - - -def _load_der_file(document_cls, f, document_name: str = None, substrate_source: str = None, parent=None): - data = f.read() - - return _load_der_document(document_cls, data, document_name, substrate_source, parent) - - -_this_module = sys.modules[__name__] - -for doc_name, doc_cls in [ - ('certificate', RFC5280Certificate), - ('crl', RFC5280CertificateList), - ('ocsp_response', RFC6960OCSPResponse), -]: - for func in [ - _load_der_file, - _load_pem_file, - _load_der_document, - _load_pem_document, - ]: - if func == _load_der_file: - func_name = f'load_der_{doc_name}_file' - elif func == _load_pem_file: - func_name = f'load_pem_{doc_name}_file' - elif func == _load_der_document: - func_name = f'load_der_{doc_name}' - elif func == _load_pem_document: - func_name = f'load_pem_{doc_name}' + if self._is_ascii_armor_start_present(substrate): + return self.load_pem_document(substrate, document_name, substrate_source, parent) else: - raise ValueError(f'Unknown function: {func}') + return self.load_b64_document(substrate, document_name, substrate_source, parent) - setattr( - _this_module, - func_name, - functools.partial(func, doc_cls) - ) + def load_file(self, f, document_name: str = None, substrate_source: str = None, parent=None): + substrate = f.read() + return self.load_document(substrate, document_name, substrate_source, parent) -def _load_document(der_loader, pem_loader, substrate, name=None, substrate_source=None, parent=None): - if isinstance(substrate, str): - return pem_loader(substrate, name, substrate_source, parent) - elif isinstance(substrate, bytes): + def load_document_or_file(self, substrate, document_name: str = None, substrate_source: str = None, parent=None): try: - return der_loader(substrate, name, substrate_source, parent) - except ValueError: - pem_str = substrate.decode() - - return pem_loader(pem_str, name, substrate_source, parent) - else: - data = substrate.read() - - return _load_document(der_loader, pem_loader, data, name, substrate_source, parent) - - -def load_certificate(io, substrate_source, name=None, parent=None): - return _load_document(getattr(_this_module, 'load_der_certificate'), getattr(_this_module, 'load_pem_certificate'), - io, name, substrate_source, parent) - - -def load_crl(io, substrate_source, name=None, parent=None): - return _load_document(getattr(_this_module, 'load_der_crl'), getattr(_this_module, 'load_pem_crl'), - io, name, substrate_source, parent) - - -def load_ocsp_response(io, substrate_source, name=None, parent=None): - return _load_document(getattr(_this_module, 'load_der_ocsp_response'), - getattr(_this_module, 'load_pem_ocsp_response'), io, name, substrate_source, parent) + return self.load_file(substrate, document_name, substrate_source, parent) + except AttributeError: + return self.load_document(substrate, document_name, substrate_source, parent) + + +# RFC 5280 Certificate +_RFC5280_CERTIFICATE_LOADER = DocumentLoader(RFC5280Certificate, 'CERTIFICATE') +load_der_certificate = _RFC5280_CERTIFICATE_LOADER.load_der_document +load_pem_certificate = _RFC5280_CERTIFICATE_LOADER.load_pem_document +load_b64_certificate = _RFC5280_CERTIFICATE_LOADER.load_b64_document +load_certificate = _RFC5280_CERTIFICATE_LOADER.load_document_or_file +load_der_certificate_file = _RFC5280_CERTIFICATE_LOADER.load_der_file +load_pem_certificate_file = _RFC5280_CERTIFICATE_LOADER.load_pem_file +load_b64_certificate_file = _RFC5280_CERTIFICATE_LOADER.load_b64_file +load_certificate_file = _RFC5280_CERTIFICATE_LOADER.load_file + + +# RFC 5280 CRL +_RFC5280_CERTIFICATE_LIST_LOADER = DocumentLoader(RFC5280CertificateList, 'X509 CRL') +load_der_crl = _RFC5280_CERTIFICATE_LIST_LOADER.load_der_document +load_pem_crl = _RFC5280_CERTIFICATE_LIST_LOADER.load_pem_document +load_b64_crl = _RFC5280_CERTIFICATE_LIST_LOADER.load_b64_document +load_crl = _RFC5280_CERTIFICATE_LIST_LOADER.load_document_or_file +load_der_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER.load_der_file +load_pem_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER.load_pem_file +load_b64_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER.load_b64_file +load_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER.load_file + + +# RFC 6960 OCSP Response +_RFC6960_OCSP_RESPONSE_LOADER = DocumentLoader(RFC6960OCSPResponse, 'OCSP RESPONSE') +load_der_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER.load_der_document +load_pem_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER.load_pem_document +load_b64_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER.load_b64_document +load_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER.load_document_or_file +load_der_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER.load_der_file +load_pem_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER.load_pem_file +load_b64_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER.load_b64_file +load_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER.load_file diff --git a/pkilint/rest/__init__.py b/pkilint/rest/__init__.py index e90bbb4..2594566 100644 --- a/pkilint/rest/__init__.py +++ b/pkilint/rest/__init__.py @@ -7,7 +7,7 @@ from pkilint.rest import model _PKILINT_VERSION = version('pkilint') -_API_VERSION = 'v1.3' +_API_VERSION = 'v1.4' app = FastAPI( title='pkilint API', diff --git a/pkilint/rest/model.py b/pkilint/rest/model.py index d405785..72984db 100644 --- a/pkilint/rest/model.py +++ b/pkilint/rest/model.py @@ -116,10 +116,8 @@ def validate(self) -> 'CertificateModel': except PyAsn1Error as e: raise ValueError('Invalid PEM text specified') from e else: - b64 = base64.b64decode(self.b64) - try: - self._parsed_document = loader.load_der_certificate(b64, 'request', 'request') + self._parsed_document = loader.load_b64_certificate(self.b64, 'request', 'request') except PyAsn1Error as e: raise ValueError('Invalid Base-64 encoding specified') from e @@ -139,13 +137,12 @@ def validate(self) -> 'OcspResponseModel': if self.pem is not None: try: - self._parsed_document = loader.load_ocsp_response(self.pem, 'request', 'request') + self._parsed_document = loader.load_pem_ocsp_response(self.pem, 'request', 'request') except PyAsn1Error as e: raise ValueError('Invalid PEM text specified') from e else: - ocsp_der_response = base64.b64decode(self.b64) try: - self._parsed_document = loader.load_ocsp_response(ocsp_der_response, 'request', 'request') + self._parsed_document = loader.load_b64_ocsp_response(self.b64, 'request', 'request') except PyAsn1Error as e: raise ValueError('Invalid Base-64 encoding specified') from e return self diff --git a/setup.cfg b/setup.cfg index 2529679..f8d1862 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ long_description_content_type = text/markdown license = MIT platform = any classifiers = - Development Status :: 4 - Beta + Development Status :: 5 - Production/Stable Intended Audience :: Information Technology License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tests/integration_certificate/etsi/qevcp_w_eidas_pre_certificate/psd2_policy_oid_present.crttest b/tests/integration_certificate/etsi/qevcp_w_eidas_pre_certificate/psd2_policy_oid_present.crttest new file mode 100644 index 0000000..13fe6a8 --- /dev/null +++ b/tests/integration_certificate/etsi/qevcp_w_eidas_pre_certificate/psd2_policy_oid_present.crttest @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIF5jCCBM6gAwIBAgIKTITWVCAMG9n4rDANBgkqhkiG9w0BAQsFADBLMQswCQYD +VQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMM +FEJ1eXBhc3MgQ2xhc3MgMyBDQSAyMB4XDTI0MDIxNDEwMzQyMFoXDTI1MDIxMzIy +NTkwMFowgfIxCzAJBgNVBAYTAk5PMQ0wCwYDVQQHDARPU0xPMQ0wCwYDVQQRDAQw +OTc4MT8wPQYDVQQKDDZNQVNURVJDQVJEIFBBWU1FTlQgU0VSVklDRVMgSU5GUkFT +VFJVQ1RVUkUgKE5PUldBWSkgQVMxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0 +aW9uMRMwEQYLKwYBBAGCNzwCAQMTAk5PMRIwEAYDVQQFEwk5MjI5ODg4NjIxGDAW +BgNVBGETD05UUk5PLTkyMjk4ODg2MjEiMCAGA1UEAxMZbXRmLm1jZW5ldHQubWFz +dGVyY2FyZC5ubzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPkzQWj +5Z5lDiodPsk7pYN7j9g9n1lYhzCepFMHJgiyacdcQxVkunYgGlB8m6CcFUcTVbu7 +qkkhN1g1gEKX3wMZVjwrSfmSoAuGsUciOS90T30HkOiDvzVxmnE9TuyhDHRKvANp +OpNNcUlJHbPondkslHvf3xEO3Q+40M65NjH9GEtepU0aEixKCjPSxnWiwiMM6PFC +NRtLJu+aQrP1wHOj2eHbaAstiTf20NKTfGbAFGVlskQcPTkNOYe2unZCUsVNyQwW +KKbaa4Dk1g8SNpiIbyeovsVMS41Ul+/wg3qoT6G7CJd5UBzUFyjnKPz/OFjALOwW +YCwNDKkmNEOtA/UCAwEAAaOCAiIwggIeMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw +FoAUIjAu0vv2S8rAuDvSBMTpcuaXmwwwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW +MBQGCCsGAQUFBwMBBggrBgEFBQcDAjBSBgNVHSAESzBJMDEGBWeBDAEBMCgwJgYI +KwYBBQUHAgEWGmh0dHBzOi8vd3d3LmJ1eXBhc3Mubm8vY3BzMAkGBwQAi+xAAQQw +CQYHBACBmCcDATA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLmJ1eXBhc3Mu +bm8vY3JsL0JQQ2xhc3MzQ0EyLmNybDAkBgNVHREEHTAbghltdGYubWNlbmV0dC5t +YXN0ZXJjYXJkLm5vMGoGCCsGAQUFBwEBBF4wXDAjBggrBgEFBQcwAYYXaHR0cDov +L29jc3AuYnV5cGFzcy5jb20wNQYIKwYBBQUHMAKGKWh0dHA6Ly9jcnQuYnV5cGFz +cy5uby9jcnQvQlBDbGFzczNDQTIuY2VyMGYGCCsGAQUFBwEDBFowWDAIBgYEAI5G +AQEwEwYGBACORgEGMAkGBwQAjkYBBgMwNwYGBACORgEFMC0wKxYlaHR0cHM6Ly93 +d3cuYnV5cGFzcy5uby9wZHMvcGRzX2VuLnBkZhMCZW4wEwYKKwYBBAHWeQIEAwEB +/wQCBQAwHwYFZ4EMAwEEFjAUEwNOVFITAk5PDAk5MjI5ODg4NjIwDQYJKoZIhvcN +AQELBQADggEBAKdqLvWe++iFFw2W+EF/s5BIMiNRc0uR+1qzuvv3iEpZ5cOvZWCM +umBt4gH2WuMC29ubMqOa3YGoC+tPOZIi7USlYOaopVugiaUEI0kFZeOVbEhyathV +Tde3FGMrENgJQRjZonxQ3Sy9AKGOnTm4SYkjuf9a/9QU1tldf2yEtT87Cq61MDdz +SpoLMv8u7c6HHZ8UA5aHbQWCtFXROk2sYwNWO3ZoFgDkdHcoB0atl841ktOT1i0L +L9hizOvNLqiq8cP1+ql0qrt6WRedEdaxkhsDRaaG10CEgI7GS+QeJV/gnvmnc3KT +D7PCXopVFZZwan4EznmLPhLy02J9g6T7g24= +-----END CERTIFICATE----- + +node_path,validator,severity,code,message +certificate.tbsCertificate.subject.rdnSequence,EvSubscriberAttributeAllowanceValidator,WARNING,cabf.ev_guidelines.common_name_attribute_present, +certificate.tbsCertificate.extensions.4.extnValue.certificatePolicies,Psd2CertificatePolicyOidPresenceValidator,ERROR,etsi.ts_119_495.ovr-6.1-3.prohibited_psd2_policy_oid_present,"Certificate type is ""QEVCP_W_EIDAS_PRE_CERTIFICATE"" but PSD2 policy identifier is present" +certificate.tbsCertificate.extensions.4.extnValue.certificatePolicies.0.policyQualifiers.0,CertificatePolicyQualifierValidator,WARNING,cabf.serverauth.certificate_policy_qualifier_present, +certificate.tbsCertificate.extensions,SubscriberExtensionAllowanceValidator,WARNING,cabf.serverauth.subscriber.unknown_extension_present,Unknown extension present: 1.3.6.1.5.5.7.1.3 +certificate.tbsCertificate.extensions,SubscriberExtensionAllowanceValidator,WARNING,cabf.serverauth.subscriber.unknown_extension_present,Unknown extension present: 2.23.140.3.1 diff --git a/tests/integration_certificate/smime_br/individual/strict/insignificant_attribute_value.crttest b/tests/integration_certificate/smime_br/individual/strict/insignificant_attribute_value.crttest new file mode 100644 index 0000000..8e35452 --- /dev/null +++ b/tests/integration_certificate/smime_br/individual/strict/insignificant_attribute_value.crttest @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIF1zCCA7+gAwIBAgIUOTexnaThhALqNKiaXDhQLJ/ZBXcwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCVVMxHzAdBgNVBAoMFkZvbyBJbmR1c3RyaWVzIExpbWl0 +ZWQxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAeFw0yMzA0MTkwMDAwMDBaFw0y +MzA3MTgyMzU5NTlaMFgxDzANBgNVBAQMBllhbWFkYTEPMA0GA1UEKgwGSGFuYWtv +MQowCAYDVQQDDAEuMSgwJgYJKoZIhvcNAQkBFhloYW5ha28ueWFtYWRhQGV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPnoGUOnrpiS +qt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqsotWxQYLEYzNEx5ZSHTGypibVsJylv +CfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE89FU1nZQF15oVLOpUgA7wGiHuEVa +wrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNUl86BU02vlBiESxOuox+dWmuVV7vf +YZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9sB6iDjj70HFldzOQ9r8SRI+9Nirup +PTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P593VVJvnzOjaA1z6Cz+4+eRvcysqhr +RgFlwI9TEwIDAQABo4IBpzCCAaMwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC +B4AwHwYDVR0jBBgwFoAU1kQAMnyoDf+sT2tm7rWumyzFOFQwHQYDVR0OBBYEFIkZ +WV4O8Wn1y71H4TT84pjMaTCRMBQGA1UdIAQNMAswCQYHZ4EMAQUEAzA9BgNVHR8E +NjA0MDKgMKAuhixodHRwOi8vY3JsLmNhLmV4YW1wbGUuY29tL2lzc3VpbmdfY2Ff +Y3JsLmNybDBLBggrBgEFBQcBAQQ/MD0wOwYIKwYBBQUHMAKGL2h0dHA6Ly9yZXBv +c2l0b3J5LmNhLmV4YW1wbGUuY29tL2lzc3VpbmdfY2EuZGVyMBMGA1UdJQQMMAoG +CCsGAQUFBwMEMIGLBgNVHREEgYMwgYCBGWhhbmFrby55YW1hZGFAZXhhbXBsZS5j +b22gJgYIKwYBBQUHCAmgGgwY5bGx55Sw6Iqx5a2QQGV4YW1wbGUuY29tpDswOTEP +MA0GA1UEBAwG5bGx55SwMQ8wDQYDVQQqDAboirHlrZAxFTATBgNVBAMMDOWxseeU +sOiKseWtkDANBgkqhkiG9w0BAQsFAAOCAgEAbPrqwt8aRFluaF5JUceC8+LS5rDr +644ITGfJ+5KHJNo/O5HRjOj+ndAsGyDgL0YuK2vQcP/r4IZ5kGeXFrc1a+srwo7u +cnqX9RzJ4IQZ/q05W75sDtLd9uZeX734tlHkTlnCl+rBrF0g2Qjwe7/rI353OeXb +KtG94aMVr4D70zdJq4w1fyms7do/GFv9JwI7+uuIpqjTf0lYvoWqnNwa1BozUaXz +7WvvSKhE8Q6lQwXLQWRdTt5FAii0Rv8bfW7dKSmNJxrbRDfsF2aX568EaQODnJMx +x4R+dWZaubT7R5ifJNgQb6wKhu+8Eeir2z+Y2YFDSs++DU/m3kFSD1aTOulLmgx5 +a2YyDLpdLMU45EaK9KuYK0mkUT4IvjJJ8wEfnjMB8A9pon3zDe6Pzfp1KVv2jTXn +UCcyf47sPGuVR21ahGJWR1TElhyWTxAPpgRyeWjH+YR/brCxTcamT6W4l7Ltiy07 +0K0MshytXojU3OuCFJcAnamYZ3RRQKLDyPZFKGGJ/Q1Rls/j9Oc62K5j5vJP5LZD +nROZ1xUqXK42ntZQcQnw1HWEDIASkb/v7enAY/UjDKY9AcAvswvehzTA8szeGWgj +5IwNCdtl7vQRGLM29OjYRf1SNm0Ds1IT2KtlyHT9GnaST7Jx7Gch2F2P04nkYmgC +Hvi6SnsfcUgkdTU= +-----END CERTIFICATE----- + +node_path,validator,severity,code,message +certificate.tbsCertificate.subject.rdnSequence.2.0,SignificantAttributeValueValidator,ERROR,cabf.insignificant_attribute_value_present,"Insignificant attribute value: "".""" +certificate.tbsCertificate.extensions.3.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified, diff --git a/tests/integration_certificate/smime_br/organization/multipurpose/orgid_and_countryname_same_different_case.crttest b/tests/integration_certificate/smime_br/organization/multipurpose/orgid_and_countryname_same_different_case.crttest index 3a4974b..65bb837 100644 --- a/tests/integration_certificate/smime_br/organization/multipurpose/orgid_and_countryname_same_different_case.crttest +++ b/tests/integration_certificate/smime_br/organization/multipurpose/orgid_and_countryname_same_different_case.crttest @@ -37,4 +37,6 @@ NvdzV/MqQUERWI3VEdQ= -----END CERTIFICATE----- node_path,validator,severity,code,message -certificate.tbsCertificate.extensions.3.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified, \ No newline at end of file +certificate.tbsCertificate.extensions.3.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified, +certificate.tbsCertificate.subject.rdnSequence.0.0,ValidCountryValidator,ERROR,cabf.invalid_country_code,"Invalid country code: ""us""" +certificate.tbsCertificate.subject.rdnSequence.0.0.value.x520countryName,OrganizationIdentifierCountryNameConsistentValidator,ERROR,cabf.smime.org_identifier_and_country_name_attribute_inconsistent,"CountryName attribute value: ""us"", OrganizationIdentifier attribute country name value: ""US""" diff --git a/tests/integration_certificate/tls_br/root_ca/basicconstraints_pathlenconstraint_present.crttest b/tests/integration_certificate/tls_br/root_ca/basicconstraints_pathlenconstraint_present.crttest index 7b51ce1..b27a176 100644 --- a/tests/integration_certificate/tls_br/root_ca/basicconstraints_pathlenconstraint_present.crttest +++ b/tests/integration_certificate/tls_br/root_ca/basicconstraints_pathlenconstraint_present.crttest @@ -39,3 +39,4 @@ certificate.tbsCertificate.signature,ServerauthAllowedSignatureAlgorithmEncoding certificate.tbsCertificate.extensions.2.extnValue.basicConstraints,RootBasicConstraintsValidator,WARNING,cabf.serverauth.root_basic_constraints_pathlenconstraint_present, certificate.tbsCertificate.extensions,RootExtensionAllowanceValidator,WARNING,cabf.serverauth.root.unknown_extension_present,Unknown extension present: 2.5.29.33 certificate.tbsCertificate.extensions.4.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified, +certificate.tbsCertificate.subject.rdnSequence.0.0,ValidCountryValidator,ERROR,cabf.invalid_country_code,"Invalid country code: ""ch""" diff --git a/tests/test_loader.py b/tests/test_loader.py new file mode 100644 index 0000000..f835026 --- /dev/null +++ b/tests/test_loader.py @@ -0,0 +1,240 @@ +import tempfile + +from pkilint import loader +import base64 + + +_CERT_B64 = '''MIIGrzCCBJegAwIBAgIUYsQ+Fan+RfQ1ToEaA+PeZh43OTEwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCVVMxHzAdBgNVBAoMFkZvbyBJbmR1c3RyaWVzIExpbWl0 +ZWQxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAeFw0yMzA0MTkwMDAwMDBaFw0y +MzA3MTgyMzU5NTlaMIGpMSMwIQYDVQRhExpMRUlYRy1BRVlFMDBFS1hFU1ZaVVVF +QlA2NzEeMBwGA1UEChMVQWNtZSBJbmR1c3RyaWVzLCBMdGQuMQ8wDQYDVQQEDAZZ +YW1hZGExDzANBgNVBCoMBkhhbmFrbzEWMBQGA1UEAwwNWUFNQURBIEhhbmFrbzEo +MCYGCSqGSIb3DQEJARYZaGFuYWtvLnlhbWFkYUBleGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALD56BlDp66YkqreF8p8QPh0T+0vgUjm +yOqie30AFUj7UZKrKLVsUGCxGMzRMeWUh0xsqYm1bCcpbwn7k6A03zLpfG/wmYz9 +jm9C3aWKzR+peYbxRPPRVNZ2UBdeaFSzqVIAO8Boh7hFWsKxn3svdlBOvJjslFVx +sHiSFQ3canTKD7zTVJfOgVNNr5QYhEsTrqMfnVprlVe732Ge/U6Ify1CuN2LyYfq +4b+Jyrhe4h41YwXfbAeog44+9BxZXczkPa/EkSPvTYq7qT05BeQCjXupFISidZbg +e0tu2ZLwd7Uk09z+fd1VSb58zo2gNc+gs/uPnkb3MrKoa0YBZcCPUxMCAwEAAaOC +Ai0wggIpMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMB8GA1UdIwQYMBaA +FNZEADJ8qA3/rE9rZu61rpssxThUMB0GA1UdDgQWBBSJGVleDvFp9cu9R+E0/OKY +zGkwkTAUBgNVHSAEDTALMAkGB2eBDAEFAwMwPQYDVR0fBDYwNDAyoDCgLoYsaHR0 +cDovL2NybC5jYS5leGFtcGxlLmNvbS9pc3N1aW5nX2NhX2NybC5jcmwwSwYIKwYB +BQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vcmVwb3NpdG9yeS5jYS5leGFt +cGxlLmNvbS9pc3N1aW5nX2NhLmRlcjATBgNVHSUEDDAKBggrBgEFBQcDBDCB2AYD +VR0RBIHQMIHNgRloYW5ha28ueWFtYWRhQGV4YW1wbGUuY29toCYGCCsGAQUFBwgJ +oBoMGOWxseeUsOiKseWtkEBleGFtcGxlLmNvbaSBhzCBhDEjMCEGA1UEYRMaTEVJ +WEctQUVZRTAwRUtYRVNWWlVVRUJQNjcxJDAiBgNVBAoMG+OCouOCr+ODn+W3peal +reagquW8j+S8muekvjEPMA0GA1UEBAwG5bGx55SwMQ8wDQYDVQQqDAboirHlrZAx +FTATBgNVBAMMDOWxseeUsOiKseWtkDAjBgkrBgEEAYOYKgEEFhMUQUVZRTAwRUtY +RVNWWlVVRUJQNjcwEgYJKwYBBAGDmCoCBAUTA0NFTzANBgkqhkiG9w0BAQsFAAOC +AgEAE/8rQdESC9lQcnw5TnIj/DhzWqrE6S4I1F7LFgUNQB5GJUSUbnFdeExwfV+t +bjloht4frY7oJvvYyjT2t5/nv2Hrfpe95KmRhliEkEfs3ri5J/pMHa5ju1Kox49n +m8OjKkon9HMK6c7IJy2Ow1yrwDYDflVeMmZUvMr+EmUk6BdRtF40ljNwLw8xJZfh +xUzo1OjaTKu7gtYqzrFhEqijpVoxtWIBLgL7IAujPYONrxeffJ7DY6vWzBVG4C+7 +iuqlrf6Y2f25yfEp0Hs9kBD26xEZUg43Zl7BxaBbJLesUk2FRD1B/N5DYZecTc7W +F1a1YUW5N15wskn8SZAXIz9xx8OThu9v7eP3qpUNaU+iaTqbjxTPGiSUYa3Jrm1y +Abh4XCOUfb4UJo23uHsNZyoLOX8lVOsesLOE/BGvlKHzT0x49uNKZq0O6lU9fxFt +iM4MRNqmNZTN9jZ1yu06cuI8nr8AEWt7Hp5OTldj5KXZFd945DqWyZHx01Uv/w5Z +U8/E3Jf1bDTbf5OLWqombrgLIWL+A/SrRvnqyLpyDv2PHJ0IgbsylDRalxeGHa1Q +3egwHqkYRzYOy3LYRphJITSGCnqRGshySonks4osE7KbXFwMEEmEWlF1S7S+VDkq +Eqpda1II90v7ae6kNwIPK+140WOhkKilZ526OHvetaZ9XUc=''' + + +_OCSP_RESPONSE_B64 = '''MIIDnwoBAKCCA5gwggOUBgkrBgEFBQcwAQEEggOFMIIDgTCBsKIWBBQK46D+ndQl +dpi163Lrygznvz318RgPMjAyNDA0MDIxMjM3NDdaMIGEMIGBMFkwDQYJYIZIAWUD +BAIBBQAEIDqZRndWgHOnB7/eUBhjReTNYTTbCF66odEEJfA7bwjqBCBHSmyjAfI9 +yff3B4cE4cf1/JbnFnX27YguerZcP1hFQwIEAarwDYAAGA8yMDI0MDQwMzEyMzc0 +N1qgERgPMjAyNDA0MTAxMjM3NDdaMAoGCCqGSM49BAMDA2kAMGYCMQDRmVmiIb4D +m9yEXiv2XtoeQi6ftpjLmlBqqRIi+3htfF/OyjdHnFuh38cQKYqqrWYCMQDKiPct +Vu7SQs587d2ZBEHQH20j5AFiGGsbI1b3+C9ZK6NIzgD6DnWlDwpSfilEarOgggJT +MIICTzCCAkswggGuoAMCAQICAQEwCgYIKoZIzj0EAwQwODELMAkGA1UEBhMCWFgx +FDASBgNVBAoMC0NlcnRzICdyIFVzMRMwEQYDVQQDDApJc3N1aW5nIENBMB4XDTI0 +MDQwMjEyMzc0N1oXDTI1MDQwMjEyMzc0N1owPDELMAkGA1UEBhMCWFgxFDASBgNV +BAoMC0NlcnRzICdyIFVzMRcwFQYDVQQDDA5PQ1NQIFJlc3BvbmRlcjB2MBAGByqG +SM49AgEGBSuBBAAiA2IABFsJAbiFIyluuRnVD/oanLN0vE1AlYYoK/7KEbHZWtu1 +RzSvVwv4K3IozyJrz0wl3bz+Oxo605Qw7/dj4daNLhUdkXILd5W1jaazRjlhOo+5 +tajaSMZ0cRf5kZ6EJPN+yKOBhzCBhDAdBgNVHQ4EFgQUCuOg/p3UJXaYtety68oM +57899fEwHwYDVR0jBBgwFoAUjsIUCWB26pA46TmuG21SxBd9n74wDAYDVR0TAQH/ +BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwkwDwYJKwYB +BQUHMAEFBAIFADAKBggqhkjOPQQDBAOBigAwgYYCQRQqjNYKbGXHdGXfEVvB//i+ +DiG02hraU9kGNKXeiQcPdZRajQsY/hdZPVyaykkAFVQGv29yWmTrEax+r4oZTtzG +AkFJCwtJpi7m00Qx9r/ugNWsnCFSiKUdxuvj7mg9lJtz0hexRJZKFODWJG5dUh// +Bc2w8vywgYYoduXu4QLcoP17CA==''' + + +_CRL_B64 = '''MIIBzTCBtgIBATANBgkqhkiG9w0BAQsFADAiMQswCQYDVQQGEwJYWDETMBEGA1UE +CgwKQ1JMcyAnciBVcxcNMjQwMzI1MTg0NzAwWhcNMjQwNDAxMTg0NzAwWqBgMF4w +CgYDVR0UBAMCAQEwHwYDVR0jBBgwFoAU/NE0t8uklbG2WeoLBWIe6JqPtDowLwYD +VR0cAQH/BCUwI6AeoByGGmh0dHA6Ly9mb28uZXhhbXBsZS9jcmwuZGxshAH/MA0G +CSqGSIb3DQEBCwUAA4IBAQAN8oDSvWsg3JvUJ4MkXvczaFb72VH0J/VL5PV2cBSm +MfaVBKnUsNr1IcxT06KF8gNrDTpKqJ9fetO290swZfcPt9sEVUBVQUpdlQc3tya1 +jYWmFkA3tkpqH5rBCQa3CBm1Cg8cbFBtwWgWr70NsVvfD6etjAEP9Ze+MSXnGV0p +w9EeOV07HnSD/PGQwqCiaSn5DdIDVoH8eFSGmgNLw+b4SwUjmz8PqsZwvHxJvleV +1D8cj7zdR4ywgRMjEfJZ8Bp+Tdu64Gv0doDS0iEJIshLHYkcW1okpq/tPm8kKAbD +reparePNQwhScVcDiSL73eEBIPokgG3QhohiucP5MeF1''' + + +def _make_pem(b64, pem_label): + return f'-----BEGIN {pem_label}-----\n{b64}\n-----END {pem_label}-----' + + +def _load_and_compare(loader_func, expected_doc_cls, expected_substrate): + loaded = loader_func() + + assert isinstance(loaded, expected_doc_cls) + assert loaded.substrate == expected_substrate + + +def _test_loader_obj(loader_instance, doc_b64): + doc_cls = loader_instance._document_cls + label = loader_instance._document_pem_label + + doc_pem = _make_pem(doc_b64, label) + doc_der = base64.b64decode(doc_b64) + + _load_and_compare(lambda: loader_instance.load_b64_document(doc_b64, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_pem_document(doc_pem, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_der_document(doc_der, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document(doc_b64, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document(doc_pem, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document(doc_der, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document_or_file(doc_b64, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document_or_file(doc_pem, 'test'), doc_cls, doc_der) + _load_and_compare(lambda: loader_instance.load_document_or_file(doc_der, 'test'), doc_cls, doc_der) + + # format-specific file load + with tempfile.TemporaryFile('w+') as f: + f.write(doc_b64) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_b64_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_b64.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_b64_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+') as f: + f.write(doc_pem) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_pem_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_pem.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_pem_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_der) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_der_file(f, 'test'), doc_cls, doc_der) + + # format-agnostic load + with tempfile.TemporaryFile('w+') as f: + f.write(doc_b64) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_b64.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+') as f: + f.write(doc_pem) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_pem.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_der) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_file(f, 'test'), doc_cls, doc_der) + + # format-agnostic file or document load + with tempfile.TemporaryFile('w+') as f: + f.write(doc_b64) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_document_or_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_b64.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_document_or_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+') as f: + f.write(doc_pem) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_document_or_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_pem.encode()) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_document_or_file(f, 'test'), doc_cls, doc_der) + + with tempfile.TemporaryFile('w+b') as f: + f.write(doc_der) + + f.flush() + f.seek(0) + + _load_and_compare(lambda: loader_instance.load_document_or_file(f, 'test'), doc_cls, doc_der) + + +def test_certificate_loader(): + _test_loader_obj(loader._RFC5280_CERTIFICATE_LOADER, _CERT_B64) + + +def test_crl_loader(): + _test_loader_obj(loader._RFC5280_CERTIFICATE_LIST_LOADER, _CRL_B64) + + +def test_ocsp_response_loader(): + _test_loader_obj(loader._RFC6960_OCSP_RESPONSE_LOADER, _OCSP_RESPONSE_B64) diff --git a/tests/test_server.py b/tests/test_server.py index 98621e7..e138a1f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,7 +1,7 @@ -import pytest from http import HTTPStatus from importlib.metadata import version +import pytest from fastapi.testclient import TestClient from pkilint import report, pkix @@ -143,7 +143,7 @@ def test_version(client): ''' -_OCSP_RESPONSE = '''MIIDnwoBAKCCA5gwggOUBgkrBgEFBQcwAQEEggOFMIIDgTCBsKIWBBQK46D+ndQl +_OCSP_RESPONSE_B64 = '''MIIDnwoBAKCCA5gwggOUBgkrBgEFBQcwAQEEggOFMIIDgTCBsKIWBBQK46D+ndQl dpi163Lrygznvz318RgPMjAyNDA0MDIxMjM3NDdaMIGEMIGBMFkwDQYJYIZIAWUD BAIBBQAEIDqZRndWgHOnB7/eUBhjReTNYTTbCF66odEEJfA7bwjqBCBHSmyjAfI9 yff3B4cE4cf1/JbnFnX27YguerZcP1hFQwIEAarwDYAAGA8yMDI0MDQwMzEyMzc0 @@ -165,6 +165,9 @@ def test_version(client): Bc2w8vywgYYoduXu4QLcoP17CA==''' +_OCSP_RESPONSE_PEM = f'''-----BEGIN OCSP RESPONSE-----\n{_OCSP_RESPONSE_B64}\n-----END OCSP RESPONSE-----\n''' + + def _assert_validationerror_list_present(resp): j = resp.json() @@ -401,7 +404,16 @@ def test_ocsp_pkix_validations_list(client): def test_ocsp_pkix_lint(client): - resp = client.post('/ocsp/pkix', json={'b64': _OCSP_RESPONSE}) + resp = client.post('/ocsp/pkix', json={'b64': _OCSP_RESPONSE_B64}) + assert resp.status_code == HTTPStatus.OK + + j = resp.json() + + assert len(j['results']) == 0 + + +def test_ocsp_pkix_lint_pem(client): + resp = client.post('/ocsp/pkix', json={'pem': _OCSP_RESPONSE_PEM}) assert resp.status_code == HTTPStatus.OK j = resp.json() @@ -409,6 +421,11 @@ def test_ocsp_pkix_lint(client): assert len(j['results']) == 0 +def test_ocsp_pkix_lint_b64_in_pem_field(client): + resp = client.post('/ocsp/pkix', json={'pem': _OCSP_RESPONSE_B64}) + assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + def test_detect_and_lint_etsi(client): resp = client.post('/certificate/etsi', json={'pem': _OV_FINAL_CLEAN_PEM}) assert resp.status_code == HTTPStatus.OK