Skip to content

Commit

Permalink
Release v0.11.1 (#85)
Browse files Browse the repository at this point in the history
* Proposed release v0.11.1

* Fix bug in Base64 file loader, add loader tests

* Adjust significant attribute validator to allow more symbol classes

* Update CHANGELOG
  • Loading branch information
CBonnell authored Jul 2, 2024
1 parent 8626d59 commit ebf135b
Show file tree
Hide file tree
Showing 31 changed files with 581 additions and 221 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.0
0.11.1
27 changes: 0 additions & 27 deletions pkilint/bin/convert_cert.py

This file was deleted.

2 changes: 1 addition & 1 deletion pkilint/bin/lint_cabf_serverauth_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkilint/bin/lint_cabf_smime_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkilint/bin/lint_crl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkilint/bin/lint_etsi_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkilint/bin/lint_ocsp_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkilint/bin/lint_pkix_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions pkilint/bin/lint_pkix_signer_signee_cert_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,19 @@ 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

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
Expand Down
35 changes: 32 additions & 3 deletions pkilint/cabf/cabf_name.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
Expand Down Expand Up @@ -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}"'
)
1 change: 1 addition & 0 deletions pkilint/cabf/serverauth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def create_subject_name_validators() -> List[validation.Validator]:
serverauth_name.ValidBusinessCategoryValidator(),
cabf_name.CabfOrganizationIdentifierAttributeValidator(),
serverauth_name.ServerauthRelativeDistinguishedNameContainsOneElementValidator(),
cabf_name.SignificantAttributeValueValidator(),
]


Expand Down
1 change: 1 addition & 0 deletions pkilint/cabf/serverauth/finding_metadata.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pkilint/cabf/smime/finding_metadata.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 16 additions & 42 deletions pkilint/cabf/smime/smime_name.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import validators
from pyasn1.type import char
from pyasn1_alt_modules import rfc5280, rfc8398

from pkilint import validation, pkix, oid
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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}", '
Expand All @@ -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
1 change: 1 addition & 0 deletions pkilint/etsi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]

Expand Down
4 changes: 4 additions & 0 deletions pkilint/etsi/en_319_411_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)

Expand All @@ -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))

Expand Down
Loading

0 comments on commit ebf135b

Please sign in to comment.