Skip to content

Commit

Permalink
v0.9.3 (#42)
Browse files Browse the repository at this point in the history
* Advertise Python 3.12 support

* Add support for "get validations for linter" GET endpoint (#36)

* Suppress ValueError stack trace in lint_cabf_smime_cert (#37)

* Use case-insensitive comparison for countryName + orgId country (#38)

* Change severity of cabf.smime.email_address_in_attribute_not_in_san to ERROR (#39)

* Remove unneeded validation now that "cabf.smime.email_address_in_attribute_not_in_san" is an ERROR

* Gracefully handle decoding errors when determining cert type in REST API (#40)

* Surround document-sourced values with double quotes in finding messages (#41)

* Add v0.9.3 changelog
  • Loading branch information
CBonnell authored Sep 20, 2023
1 parent 960fbae commit 1c3c5ad
Show file tree
Hide file tree
Showing 43 changed files with 312 additions and 86 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

ALl notable changes to this project from version 0.9.3 onwards are documented in this file.

## 0.9.3 - 2023-09-20

### New features/enhancements

- Explicitly support Python 3.12 (#34)
- Add REST endpoint that returns the set of possible findings for a specific linter (#36)
- Surround document-sourced string values with double quotes in finding messages (#41)

### Fixes

- Suppress `ValueError` stack trace when `lint_cabf_smime_cert` can't determine certificate type (#37)
- `OrganizationIdentifierCountryNameConsistentValidator` should perform a case-insensitive country comparison (#38)
- Change severity of `cabf.smime.email_address_in_attribute_not_in_san` from WARNING to ERROR (#39)
- Decoding error when determining certificate type returns HTTP 500 (#40)
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.2
0.9.3
4 changes: 3 additions & 1 deletion pkilint/bin/lint_cabf_smime_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ def main(cli_args=None) -> int:
v_g = smime.determine_validation_level_and_generation(cert, args.mapping)

if v_g is None:
raise ValueError('Could not determine validation level and generation')
print('Could not determine validation level and generation', file=sys.stderr)

return 1
else:
validation_level, generation = v_g
elif args.guess:
Expand Down
10 changes: 5 additions & 5 deletions pkilint/cabf/cabf_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,26 @@ def validate_with_value(self, node, choice_node):
if m is None:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_FORMAT,
f'Invalid format: {value_node.pdu}'
f'Invalid format: "{value_node.pdu}"'
)

scheme_info = self._allowed_schemes.get(m['scheme'])

if scheme_info is None:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_SCHEME,
f'Invalid registration scheme: {m["scheme"]}'
f'Invalid registration scheme: "{m["scheme"]}"'
)

if scheme_info.require_registration_reference and m['reference'] is None:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_FORMAT,
f'Missing Registration Reference: {value_node.pdu}'
f'Missing Registration Reference: "{value_node.pdu}"'
)
elif not scheme_info.require_registration_reference and m['reference']:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_FORMAT,
f'Prohibited Registration Reference is present: {value_node.pdu}'
f'Prohibited Registration Reference is present: "{value_node.pdu}"'
)

country_code = '' if m['country'] is None else m['country'].upper()
Expand All @@ -156,7 +156,7 @@ def validate_with_value(self, node, choice_node):
if not valid_country_code:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_COUNTRY,
f'Invalid country code for scheme "{m["scheme"]}": {country_code}'
f'Invalid country code for scheme "{m["scheme"]}": "{country_code}"'
)

if m['sp'] is not None and not scheme_info.allow_state_province:
Expand Down
2 changes: 1 addition & 1 deletion pkilint/cabf/serverauth/serverauth_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def validate(self, node):

raise validation.ValidationFindingEncountered(
self.VALIDATION_DUPLICATE_LOCATION_URI,
f'Duplicate AIA access locations: {dup_locations_str}'
f'Duplicate AIA access locations: "{dup_locations_str}"'
)


Expand Down
4 changes: 2 additions & 2 deletions pkilint/cabf/serverauth/serverauth_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def validate(self, node):
if business_category not in self._ALLOWED_VALUES:
raise validation.ValidationFindingEncountered(
self.VALIDATION_INVALID_BUSINESS_CATEGORY,
f'Invalid business category: {business_category}'
f'Invalid business category: "{business_category}"'
)


Expand Down Expand Up @@ -131,7 +131,7 @@ def validate(self, node):
if m is None:
raise validation.ValidationFindingEncountered(
self.VALIDATION_CABF_ORG_ID_INVALID_SYNTAX,
f'Invalid syntax: {attr_value}'
f'Invalid syntax: "{attr_value}"'
)

findings = []
Expand Down
4 changes: 2 additions & 2 deletions pkilint/cabf/serverauth/serverauth_subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def validate(self, node):
if scheme_info is None:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_SCHEME,
f'Invalid registration scheme: {scheme}'
f'Invalid registration scheme: "{scheme}"'
)

if scheme_info.country_identifier_type == cabf_constants.RegistrationSchemeCountryIdentifierType.NONE:
Expand All @@ -71,7 +71,7 @@ def validate(self, node):
if not valid_country_code:
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGANIZATION_ID_INVALID_COUNTRY,
f'Invalid country code for scheme "{scheme}": {country}'
f'Invalid country code for scheme "{scheme}": "{country}"'
)

if sp is not None and not scheme_info.allow_state_province:
Expand Down
3 changes: 1 addition & 2 deletions pkilint/cabf/smime/finding_metadata.csv
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ERROR,cabf.smime.common_name_value_unknown_source,SMBR 7.1.4.2.2 (a),Common name
ERROR,cabf.smime.cps_uri_is_not_http,SMBR 7.1.2.3 (a),"""If the value of this extension includes a PolicyInformation which contains a qualifier of type id-qt-cps (OID: 1.3.6.1.5.5.7.2.1), then the value of the qualifier SHALL be a HTTP or HTTPS URL for the Issuing CA�s CP and/or CPS, Relying Party Agreement, or other pointer to online policy information provided by the Issuing CA"""
ERROR,cabf.smime.crldp_fullname_prohibited_generalname_type,SMBR 7.1.2.3 (b),"""Allowed URI scheme"""
ERROR,cabf.smime.crldp_fullname_prohibited_uri_scheme,SMBR 7.1.2.3 (b),"Legacy: ""At least one uniformResourceIdentifier SHALL have the URI scheme HTTP"". MP and strict: ""Every uniformResourceIdentifier SHALL have the URI scheme HTTP"""
ERROR,cabf.smime.email_address_in_common_name_not_in_san,SMBR 7.1.4.2.2 (a),"""If present, the Mailbox Address SHALL contain a rfc822Name or otherName value of type id-on-SmtpUTF8Mailbox from extensions:subjectAltName"""
ERROR,cabf.smime.email_address_in_attribute_not_in_san,SMBR 7.1.4.2.1,"""All Mailbox Addresses in the subject field or entries of type dirName of this extension SHALL be repeated as rfc822Name or otherName values of type id-on-SmtpUTF8Mailbox in this extension"""
ERROR,cabf.smime.emailprotection_eku_missing,SMBR 7.1.2.3 (f),"""id-kp-emailProtection SHALL be present"""
ERROR,cabf.smime.extended_key_usage_extension_missing,SMBR 7.1.2.3 (f),"""SHALL be present"""
ERROR,cabf.smime.invalid_lei_scheme_format,SMBR 7.1.4.2.2 (d) and SMBR 7.1.2.3 (l),LEI value does not conform to standard LEI format (20 alphanumeric characters)
Expand Down Expand Up @@ -129,7 +129,6 @@ WARNING,cabf.rsa_exponent_not_in_recommended_range,SMBR 6.1.6,"""Additionally, t
WARNING,cabf.rsa_modulus_has_small_prime_factor,SMBR 6.1.6,"""Additionally, the public exponent SHOULD be in the range between 2^16 + 1 and 2^256 ? 1. The modulus SHOULD also have the following characteristics: an odd number, not the power of a prime, and have no factors smaller than 752."""
WARNING,cabf.smime.certificate_validity_period_at_maximum,SMBR 6.3.2,"""For this reason, Subscriber Certificates SHOULD NOT be issued for the maximum permissible time by default, in order to account for such adjustments"""
WARNING,cabf.smime.critical_crldp_extension,SMBR 7.1.2.3 (b),"""This extension SHOULD NOT be marked critical"""
WARNING,cabf.smime.email_address_in_attribute_not_in_san,SMBR 7.1.4.2.1,"""All Mailbox Addresses in the subject field or entries of type dirName of this extension SHALL be repeated as rfc822Name or otherName values of type id-on-SmtpUTF8Mailbox in this extension"". Findings of this type are likely an ERROR, but this finding is marked at WARNING-level due to the possibility of false positives."
WARNING,cabf.smime.ku_extension_not_critical,SMBR 7.1.2.3 (e),"""This extension SHOULD be marked critical"""
WARNING,pkix.certificate_crldp_extension_critical,RFC 5280 4.2.1.13,"""The extension SHOULD be non-critical"""
WARNING,pkix.certificate_policies_explicit_text_has_control_character,RFC 5280 4.2.1.4,"""The explicitText string SHOULD NOT include any control characters (e.g., U+0000 to U+001F and U+007F to U+009F)"""
Expand Down
42 changes: 10 additions & 32 deletions pkilint/cabf/smime/smime_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,8 @@ def create_subscriber_certificate_subject_validator_container(
class SubjectAlternativeNameContainsSubjectEmailAddressesValidator(
validation.Validator
):
VALIDATION_EMAIL_ADDRESS_IN_CN_MISSING_FROM_SAN = validation.ValidationFinding(
validation.ValidationFindingSeverity.ERROR,
'cabf.smime.email_address_in_common_name_not_in_san'
)

VALIDATION_EMAIL_ADDRESS_IN_ATTRIBUTE_MISSING_FROM_SAN = validation.ValidationFinding(
validation.ValidationFindingSeverity.WARNING,
validation.ValidationFindingSeverity.ERROR,
'cabf.smime.email_address_in_attribute_not_in_san'
)

Expand All @@ -252,7 +247,6 @@ class SubjectAlternativeNameContainsSubjectEmailAddressesValidator(
def __init__(self):
super().__init__(
validations=[
self.VALIDATION_EMAIL_ADDRESS_IN_CN_MISSING_FROM_SAN,
self.VALIDATION_EMAIL_ADDRESS_IN_ATTRIBUTE_MISSING_FROM_SAN,
self.VALIDATION_UNPARSED_ATTRIBUTE,
],
Expand Down Expand Up @@ -285,29 +279,13 @@ def validate(self, node):
value_str = str(value.pdu)

if bool(validators.email(value_str)):
san_ext = node.document.get_extension_by_oid(
rfc5280.id_ce_subjectAltName
)
if san_ext is None:
email_sans = set()
else:
email_sans = set((
str(gn.child[1].pdu)
for gn in san_ext[0].navigate('extnValue.subjectAltName').children.values()
if gn.child[0] == 'rfc822Name'
))

if value_str not in email_sans:
if oid == rfc5280.id_at_commonName:
raise validation.ValidationFindingEncountered(
self.VALIDATION_EMAIL_ADDRESS_IN_CN_MISSING_FROM_SAN,
value_str
)
else:
raise validation.ValidationFindingEncountered(
self.VALIDATION_EMAIL_ADDRESS_IN_ATTRIBUTE_MISSING_FROM_SAN,
f'Attribute {str(oid)} with value "{value_str}" not found in SAN'
)
san_email_addresses = get_email_addresses_from_san(node.document)

if value_str not in san_email_addresses:
raise validation.ValidationFindingEncountered(
self.VALIDATION_EMAIL_ADDRESS_IN_ATTRIBUTE_MISSING_FROM_SAN,
f'Attribute {str(oid)} with value "{value_str}" not found in SAN'
)


class CommonNameValidator(validation.Validator):
Expand Down Expand Up @@ -411,7 +389,7 @@ def validate(self, node):
if not value.startswith(self._LEI_PREFIX):
raise validation.ValidationFindingEncountered(
self.VALIDATION_INVALID_ORGID_LEI_FORMAT,
f'Invalid Organization Identifier format: {value}'
f'Invalid Organization Identifier format: "{value}"'
)

lei_value = value[len(self._LEI_PREFIX):]
Expand Down Expand Up @@ -455,7 +433,7 @@ def validate(self, node):
if not orgid_country_name or orgid_country_name.upper() == 'XG':
continue

if orgid_country_name != country_name_value:
if orgid_country_name.casefold() != country_name_value.casefold():
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGID_COUNTRYNAME_INCONSISTENT,
f'CountryName attribute value: "{country_name_value}", '
Expand Down
2 changes: 1 addition & 1 deletion pkilint/iso/lei.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def validate_lei(lei: str):

if m is None:
raise validation.ValidationFindingEncountered(
VALIDATION_INVALID_LEI_FORMAT, f'Invalid LEI format: {lei}'
VALIDATION_INVALID_LEI_FORMAT, f'Invalid LEI format: "{lei}"'
)

value_part = m.group('value')
Expand Down
2 changes: 1 addition & 1 deletion pkilint/pkix/name.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def validate(self, node):
if not isinstance(ret, bool) or not ret:
raise validation.ValidationFindingEncountered(
self.VALIDATION_NAME_DC_NOT_A_VALID_DOMAIN_NAME,
f'Invalid domain name in domainComponents: {domain_name}'
f'Invalid domain name in domainComponents: "{domain_name}"'
)


Expand Down
19 changes: 12 additions & 7 deletions pkilint/report.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import csv
import io
import json
from typing import Iterable, Optional, Any
from typing import Iterable, Optional, Any, List

from pkilint import validation
from pkilint.validation import ValidationFindingSeverity, ValidationResult, ValidationFindingDescription


Expand Down Expand Up @@ -143,17 +144,21 @@ def report_wrapper(report_generator_cls, *args, **kwargs):
_VALIDATION_LIST_CSV_FIELDNAMES = ['severity', 'code']


def report_included_validations(*args):
def get_included_validations(*args) -> List[validation.ValidationFinding]:
all_validations = set()
for validator in args:
all_validations.update(validator.validations)

return sorted(all_validations, key=lambda v: f'{int(v.severity)}-{v.code}')


def report_included_validations(*args) -> str:
s = io.StringIO()

c = csv.DictWriter(s, fieldnames=_VALIDATION_LIST_CSV_FIELDNAMES)
c.writeheader()

all_validations = set()
for validator in args:
all_validations.update(validator.validations)

validations = sorted(all_validations, key=lambda v: f'{int(v.severity)}-{v.code}')
validations = get_included_validations(*args)

for v in validations:
c.writerow({'severity': str(v.severity), 'code': v.code})
Expand Down
12 changes: 11 additions & 1 deletion pkilint/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pkilint.rest import model

_PKILINT_VERSION = version('pkilint')
_API_VERSION = 'v1'
_API_VERSION = 'v1.1'

app = FastAPI(
title='pkilint API',
Expand Down Expand Up @@ -74,6 +74,16 @@ def certificate_determine_type(linter_group_name: str, doc: model.CertificateMod
return linter_group_instance.determine_linter(parsed_doc)


@app.get('/certificate/{linter_group_name}/{linter_name}')
def linter_validations(linter_group_name: str, linter_name: str) -> List[model.Validation]:
"""Returns the set of validations performed by the specified linter"""
linter_group_instance = _get_linter_group_by_name(linter_group_name)

linter_instance = linter_group_instance.get_linter_by_name(linter_name)

return linter_instance.validations


@app.post('/certificate/{linter_group_name}/{linter_name}')
def certificate_lint(linter_group_name: str, linter_name: str, doc: model.CertificateModel) -> model.LintResultList:
"""Lints the specified certificate with the specified linter"""
Expand Down
8 changes: 7 additions & 1 deletion pkilint/rest/cabf_serverauth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import HTTPException
from pyasn1.error import PyAsn1Error

from pkilint.cabf import serverauth
from pkilint.cabf.serverauth import serverauth_constants
Expand All @@ -11,7 +12,12 @@ def __init__(self, linters):
super().__init__(name='cabf-serverauth', linters=linters)

def determine_linter(self, doc):
cert_type = serverauth.determine_certificate_type(doc)
try:
cert_type = serverauth.determine_certificate_type(doc)
except (ValueError, PyAsn1Error) as e:
message = f'Parsing error occurred: {e}'

raise HTTPException(status_code=422, detail=message)

# this doesn't fail, so we don't need to guard against not being able to determine the certificate type
return next((l for l in self.linters if l.name.casefold() == cert_type.to_option_str.casefold()))
Expand Down
8 changes: 7 additions & 1 deletion pkilint/rest/cabf_smime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import HTTPException
from pyasn1.error import PyAsn1Error

from pkilint.cabf import smime
from pkilint.cabf.smime import smime_constants
Expand All @@ -11,7 +12,12 @@ def __init__(self, linters):
super().__init__(name='cabf-smime', linters=linters)

def determine_linter(self, doc):
v_g = smime.determine_validation_level_and_generation(doc)
try:
v_g = smime.determine_validation_level_and_generation(doc)
except (ValueError, PyAsn1Error) as e:
message = f'Parsing error occurred: {e}'

raise HTTPException(status_code=422, detail=message)

if v_g is None:
raise HTTPException(status_code=422, detail='Could not determine certificate type')
Expand Down
12 changes: 12 additions & 0 deletions pkilint/rest/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class Version(BaseModel):
_SEVERITY_DESCRIPTION = f'The severity of the finding ({", ".join(map(str, validation.ValidationFindingSeverity))})'


class Validation(BaseModel):
severity: Annotated[str, Field(description=_SEVERITY_DESCRIPTION)]
code: Annotated[str, Field(description='The code that identifies the type of validation')]


class FindingDescription(BaseModel):
severity: Annotated[str, Field(description=_SEVERITY_DESCRIPTION)]
code: Annotated[str, Field(description='The code that identifies the type of finding')]
Expand Down Expand Up @@ -45,6 +50,13 @@ def __init__(self, validator, finding_filters=None, **kwargs):
self._validator = validator
self._finding_filters = finding_filters

@property
def validations(self) -> List[Validation]:
return [
Validation(severity=str(v.severity), code=v.code)
for v in report.get_included_validations(self._validator)
]

def lint(self, doc) -> LintResultList:
results = self._validator.validate(doc.root)

Expand Down
8 changes: 4 additions & 4 deletions pkilint/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,18 @@ def validate(self, node: PDUNode) -> 'ValidationResult':
pass

@property
def tags(self):
def tags(self) -> List[str]:
return ['static']

@property
def validations(self):
def validations(self) -> List[ValidationFinding]:
return self._validations + [self.VALIDATION_FINDING_UNHANDLED_EXCEPTION]

@property
def name(self):
def name(self) -> str:
return self.__class__.__name__

def __repr__(self):
def __repr__(self) -> str:
return self.name


Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12

[options]
zip_safe = True
Expand Down
Loading

0 comments on commit 1c3c5ad

Please sign in to comment.