Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v0.11.1 #85

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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