diff --git a/README.md b/README.md index db73676..e903ef0 100644 --- a/README.md +++ b/README.md @@ -151,10 +151,11 @@ The list of command line linters bundled with pkilint: Each of the linters share common command line parameters: -| Parameter | Default value | Description | -|-------------------|---------------|----------------------------------------------------------------------------------------------------| -| `-s`/`--severity` | INFO | Sets the severity threshold for findings. Findings that are below this threshold are not reported. | -| `-f`/`--format` | TEXT | Sets the format in which results will be reported. Current options are TEXT, CSV, or JSON. | +| Parameter | Default value | Description | +|---------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-s`/`--severity` | INFO | Sets the severity threshold for findings. Findings that are below this threshold are not reported. | +| `-f`/`--format` | TEXT | Sets the format in which results will be reported. Current options are TEXT, CSV, or JSON. | +| `--document-format` | DETECT | Sets the expected format of documents. If a document is not in the specified format, then an error is reported and the linter exits. Current options are BASE64, DER, PEM, and DETECT (default). | Additionally, each linter has ability to lint document (certificate, CRL, OCSP response, etc.) files as well as output the set of validations which are performed by each linter. When the `validations` sub-command is specified, the set of validations that are performed by the linter diff --git a/pkilint/bin/lint_cabf_serverauth_cert.py b/pkilint/bin/lint_cabf_serverauth_cert.py index e851ae3..e76da19 100644 --- a/pkilint/bin/lint_cabf_serverauth_cert.py +++ b/pkilint/bin/lint_cabf_serverauth_cert.py @@ -3,7 +3,7 @@ import argparse import sys -from pkilint import loader, report, util, finding_filter +from pkilint import loader, report, cli_util, finding_filter from pkilint.cabf import serverauth from pkilint.cabf.serverauth import serverauth_constants from pkilint.pkix import certificate @@ -53,9 +53,9 @@ def main(cli_args=None) -> int: lint_parser.add_argument('-r', '--report-all', action='store_true', help='Report all findings without filtering ' 'any PKIX findings that are superseded by CA/Browser Forum requirements') - util.add_certificate_validity_period_start_arg(lint_parser) + cli_util.add_certificate_validity_period_start_arg(lint_parser) - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), help='The certificate to lint' ) @@ -73,7 +73,9 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate_file(args.file, args.file.name) + cert = loader.RFC5280CertificateDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 @@ -100,7 +102,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == "__main__": diff --git a/pkilint/bin/lint_cabf_smime_cert.py b/pkilint/bin/lint_cabf_smime_cert.py index 930d8c0..41dd5da 100644 --- a/pkilint/bin/lint_cabf_smime_cert.py +++ b/pkilint/bin/lint_cabf_smime_cert.py @@ -6,7 +6,7 @@ from pyasn1.type.univ import ObjectIdentifier -from pkilint import util, loader, report +from pkilint import cli_util, loader, report from pkilint.cabf import smime from pkilint.cabf.smime import smime_constants from pkilint.pkix import certificate @@ -93,9 +93,9 @@ def main(cli_args=None) -> int: help='Output the type of S/MIME certificate to standard error. This option may be ' 'useful when using the --detect, --guess, or --mapping options.') - util.add_certificate_validity_period_start_arg(lint_parser) + cli_util.add_certificate_validity_period_start_arg(lint_parser) - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), @@ -119,7 +119,9 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate_file(args.file, args.file.name) + cert = loader.RFC5280CertificateDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 @@ -152,7 +154,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == '__main__': diff --git a/pkilint/bin/lint_crl.py b/pkilint/bin/lint_crl.py index 73e7157..ec40006 100644 --- a/pkilint/bin/lint_crl.py +++ b/pkilint/bin/lint_crl.py @@ -3,7 +3,7 @@ import argparse import sys -from pkilint import loader, pkix, report, util +from pkilint import loader, pkix, report, cli_util from pkilint.cabf import cabf_crl from pkilint.pkix import crl, name, extension @@ -34,7 +34,7 @@ def main(cli_args=None) -> int: lint_parser = subparsers.add_parser('lint', help='Lint the specified CRL') _add_args(lint_parser) - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), help='The CRL file to lint' @@ -80,7 +80,9 @@ def main(cli_args=None) -> int: return 0 else: try: - crl_doc = loader.load_crl_file(args.file, args.file.name) + crl_doc = loader.RFC5280CertificateListDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load CRL: {e}', file=sys.stderr) return 1 @@ -89,7 +91,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == "__main__": diff --git a/pkilint/bin/lint_etsi_cert.py b/pkilint/bin/lint_etsi_cert.py index 9c2ce2e..9ff3cfe 100644 --- a/pkilint/bin/lint_etsi_cert.py +++ b/pkilint/bin/lint_etsi_cert.py @@ -4,7 +4,7 @@ import sys from pkilint import etsi -from pkilint import loader, report, util, finding_filter +from pkilint import loader, report, cli_util, finding_filter from pkilint.etsi import etsi_constants from pkilint.pkix import certificate @@ -53,7 +53,7 @@ def main(cli_args=None) -> int: lint_parser.add_argument('-r', '--report-all', action='store_true', help='Report all findings without filtering ' 'any findings that are superseded by other requirements') - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), help='The certificate to lint' ) @@ -71,7 +71,9 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate_file(args.file, args.file.name) + cert = loader.RFC5280CertificateDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 @@ -98,7 +100,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == "__main__": diff --git a/pkilint/bin/lint_ocsp_response.py b/pkilint/bin/lint_ocsp_response.py index c4f0890..4441f7a 100644 --- a/pkilint/bin/lint_ocsp_response.py +++ b/pkilint/bin/lint_ocsp_response.py @@ -4,7 +4,7 @@ import sys from pkilint import pkix -from pkilint import util, loader, report +from pkilint import cli_util, loader, report from pkilint.pkix import extension, name, ocsp @@ -15,7 +15,7 @@ def main(cli_args=None) -> int: subparsers.add_parser('validations', help='Output the set of validations which this linter performs') lint_parser = subparsers.add_parser('lint', help='Lint the specified OCSP response') - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), help='The OCSP response to lint' @@ -38,7 +38,9 @@ def main(cli_args=None) -> int: return 0 else: try: - ocsp_response = loader.load_ocsp_response_file(args.file, args.file.name) + ocsp_response = loader.RFC6960OCSPResponseDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load OCSP response: {e}', file=sys.stderr) return 1 @@ -47,7 +49,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == "__main__": diff --git a/pkilint/bin/lint_pkix_cert.py b/pkilint/bin/lint_pkix_cert.py index 21aea1b..a5261e3 100644 --- a/pkilint/bin/lint_pkix_cert.py +++ b/pkilint/bin/lint_pkix_cert.py @@ -4,7 +4,7 @@ import sys from pkilint import loader -from pkilint import report, util +from pkilint import report, cli_util from pkilint.pkix import certificate, name, extension @@ -15,7 +15,7 @@ def main(cli_args=None) -> int: subparsers.add_parser('validations', help='Output the set of validations which this linter performs') lint_parser = subparsers.add_parser('lint', help='Lint the specified certificate') - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument('file', type=argparse.FileType('rb'), help='The certificate to lint' @@ -45,7 +45,9 @@ def main(cli_args=None) -> int: return 0 else: try: - cert = loader.load_certificate_file(args.file, args.file.name) + cert = loader.RFC5280CertificateDocumentLoader().get_file_loader_func(args.document_format)( + args.file, args.file.name + ) except ValueError as e: print(f'Failed to load certificate: {e}', file=sys.stderr) return 1 @@ -54,7 +56,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == "__main__": diff --git a/pkilint/bin/lint_pkix_signer_signee_cert_chain.py b/pkilint/bin/lint_pkix_signer_signee_cert_chain.py index a84bf3a..e23699b 100644 --- a/pkilint/bin/lint_pkix_signer_signee_cert_chain.py +++ b/pkilint/bin/lint_pkix_signer_signee_cert_chain.py @@ -5,7 +5,7 @@ from pyasn1_alt_modules import rfc5280 -from pkilint import loader, document, util, validation, pkix, report +from pkilint import loader, document, cli_util, validation, pkix, report from pkilint.pkix import certificate, name, extension, algorithm from pkilint.pkix.certificate import certificate_extension, certificate_key @@ -70,7 +70,7 @@ def main(cli_args=None) -> int: subparsers.add_parser('validations', help='Output the set of validations which this linter performs') lint_parser = subparsers.add_parser('lint', help='Lint the specified issuer and subject certificates') - util.add_standard_args(lint_parser) + cli_util.add_standard_args(lint_parser) lint_parser.add_argument(dest='issuer', type=argparse.FileType('rb'), help='The issuer certificate to lint' @@ -93,8 +93,10 @@ def main(cli_args=None) -> int: else: doc_collection = {} + loader_func = loader.RFC5280CertificateDocumentLoader().get_file_loader_func(args.document_format) + try: - issuer = loader.load_certificate_file( + issuer = loader_func( args.issuer, args.issuer.name, 'issuer', doc_collection ) except ValueError as e: @@ -104,7 +106,7 @@ def main(cli_args=None) -> int: doc_collection['issuer'] = issuer try: - subject = loader.load_certificate_file( + subject = loader_func( args.subject, args.subject.name, 'subject', doc_collection ) except ValueError as e: @@ -120,7 +122,7 @@ def main(cli_args=None) -> int: print(args.format(results, args.severity)) - return util.clamp_exit_code(report.get_findings_count(results, args.severity)) + return cli_util.clamp_exit_code(report.get_findings_count(results, args.severity)) if __name__ == '__main__': diff --git a/pkilint/cli_util.py b/pkilint/cli_util.py new file mode 100644 index 0000000..d0f7b9c --- /dev/null +++ b/pkilint/cli_util.py @@ -0,0 +1,141 @@ +import argparse +import datetime +import functools +from typing import Type + +import dateutil.parser + +from pkilint import validation, report, loader, document +from pkilint.pkix.certificate import certificate_validity +from pkilint.report import REPORT_FORMATS, report_wrapper + + +class SeverityThresholdAction(argparse.Action): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + if values == 'ALL': + severity = None + elif values not in [s.name for s in validation.ValidationFindingSeverity]: + raise argparse.ArgumentError(self, f'Invalid severity value: "{values}"') + else: + severity = validation.ValidationFindingSeverity[values] + + setattr(namespace, self.dest, severity) + + +def add_severity_arg(parser): + parser.add_argument('-s', '--severity', + type=str.upper, + default=validation.ValidationFindingSeverity.INFO, + help='The finding severity threshold; findings with a lesser severity will not be reported.', + action=SeverityThresholdAction, + choices=[s.name for s in validation.ValidationFindingSeverity] + ['ALL'] + ) + + +class ReportFormatAction(argparse.Action): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + report_generator_cls = REPORT_FORMATS[values] + + setattr(namespace, self.dest, functools.partial(report_wrapper, report_generator_cls)) + + +def add_report_format_arg(parser): + parser.add_argument('-f', '--format', + type=str.upper, + default=functools.partial(report_wrapper, report.ReportGeneratorPlaintext), + help='The format in which results will be output.', + choices=list(REPORT_FORMATS.keys()), + action=ReportFormatAction + ) + + +class DocumentFormatAction(argparse.Action): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + if values not in (f.name for f in loader.DocumentFormat): + raise argparse.ArgumentError(self, f'Invalid document format: "{values}"') + + document_format = loader.DocumentFormat[values] + + setattr(namespace, self.dest, document_format) + + +def add_document_format_arg(parser): + parser.add_argument('--document-format', + type=str.upper, + default=loader.DocumentFormat.DETECT, + help='The format of the specified input document(s).', + choices=[f.name for f in loader.DocumentFormat], + action=DocumentFormatAction + ) + + +class ValidityPeriodStartAction(argparse.Action): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def get_retriever_class(cls) -> Type[document.ValidityPeriodStartRetriever]: + pass + + def __call__(self, parser, namespace, values, option_string=None): + value_casefolded = values.casefold() + + if value_casefolded == 'document'.casefold(): + retriever_instance = self.get_retriever_class()() + elif value_casefolded == 'now'.casefold(): + retriever_instance = document.StaticValidityPeriodStartRetriever( + datetime.datetime.now(tz=datetime.timezone.utc) + ) + else: + try: + dt = dateutil.parser.isoparse(values) + + retriever_instance = document.StaticValidityPeriodStartRetriever(dt) + except ValueError: + raise argparse.ArgumentError(self, f'Invalid value for validity period start: "{values}"') + + setattr(namespace, self.dest, retriever_instance) + + +class CertificateValidityPeriodStartAction(ValidityPeriodStartAction): + @classmethod + def get_retriever_class(cls): + return certificate_validity.CertificateValidityPeriodStartRetriever + + +def add_validity_period_start_arg(action_cls: Type[ValidityPeriodStartAction], parser): + parser.add_argument( + '--validity-period-start', + action=action_cls, + default=action_cls.get_retriever_class()(), + help='The start of the validity period that is compared to effective dates to determine applicability of ' + 'validations. Acceptable values are "DOCUMENT" (use the validity period indicated in the document being ' + 'validated, "NOW" (use the current time), or an ISO 8601-formatted date/time value.' + ) + + +add_certificate_validity_period_start_arg = functools.partial( + add_validity_period_start_arg, + CertificateValidityPeriodStartAction +) + + +def add_standard_args(parser): + add_severity_arg(parser) + add_report_format_arg(parser) + add_document_format_arg(parser) + + +# This ensures that if a large (>255) number of findings are reported, we don't accidentally exit with an +# exit code of 0. This could happen if the number of findings is a multiple of 256. +def clamp_exit_code(exit_code): + return min(exit_code, 255) diff --git a/pkilint/loader.py b/pkilint/loader.py index 4105dc3..7208951 100644 --- a/pkilint/loader.py +++ b/pkilint/loader.py @@ -1,4 +1,5 @@ import base64 +import enum import re from pkilint.pkix.certificate import RFC5280Certificate @@ -6,6 +7,13 @@ from pkilint.pkix.ocsp import RFC6960OCSPResponse +class DocumentFormat(enum.Enum): + DETECT = enum.auto() + BASE64 = enum.auto() + DER = enum.auto() + PEM = enum.auto() + + class DocumentLoader: def __init__(self, document_cls, document_pem_label: str): self._document_cls = document_cls @@ -91,38 +99,65 @@ def load_document_or_file(self, substrate, document_name: str = None, substrate_ except AttributeError: return self.load_document(substrate, document_name, substrate_source, parent) + def get_file_loader_func(self, document_format: DocumentFormat): + if document_format == DocumentFormat.BASE64: + return self.load_b64_file + elif document_format == DocumentFormat.DER: + return self.load_der_file + elif document_format == DocumentFormat.PEM: + return self.load_pem_file + elif document_format == DocumentFormat.DETECT: + return self.load_file + else: + raise ValueError(f'Unknown document format: {document_format}') + + +class RFC5280CertificateDocumentLoader(DocumentLoader): + def __init__(self): + super().__init__(RFC5280Certificate, 'CERTIFICATE') + + +class RFC5280CertificateListDocumentLoader(DocumentLoader): + def __init__(self): + super().__init__(RFC5280CertificateList, 'X509 CRL') + + +class RFC6960OCSPResponseDocumentLoader(DocumentLoader): + def __init__(self): + super().__init__(RFC6960OCSPResponse, 'OCSP RESPONSE') + # 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 +_RFC5280_CERTIFICATE_LOADER_INSTANCE = RFC5280CertificateDocumentLoader() +load_der_certificate = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_der_document +load_pem_certificate = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_pem_document +load_b64_certificate = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_b64_document +load_certificate = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_document_or_file +load_der_certificate_file = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_der_file +load_pem_certificate_file = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_pem_file +load_b64_certificate_file = _RFC5280_CERTIFICATE_LOADER_INSTANCE.load_b64_file +load_certificate_file = _RFC5280_CERTIFICATE_LOADER_INSTANCE.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 +_RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE = RFC5280CertificateListDocumentLoader() +load_der_crl = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_der_document +load_pem_crl = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_pem_document +load_b64_crl = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_b64_document +load_crl = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_document_or_file +load_der_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_der_file +load_pem_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_pem_file +load_b64_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.load_b64_file +load_crl_file = _RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE.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 +_RFC6960_OCSP_RESPONSE_LOADER_INSTANCE = RFC6960OCSPResponseDocumentLoader() +load_der_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_der_document +load_pem_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_pem_document +load_b64_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_b64_document +load_ocsp_response = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_document_or_file +load_der_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_der_file +load_pem_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_pem_file +load_b64_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_b64_file +load_ocsp_response_file = _RFC6960_OCSP_RESPONSE_LOADER_INSTANCE.load_file diff --git a/pkilint/util.py b/pkilint/util.py index 1b4b687..0371ec1 100644 --- a/pkilint/util.py +++ b/pkilint/util.py @@ -1,15 +1,5 @@ -import argparse -import datetime -import functools -from typing import Type - -import dateutil.parser from cryptography.hazmat.primitives import hashes -from pkilint import validation, report, document -from pkilint.pkix.certificate import certificate_validity -from pkilint.report import report_wrapper, REPORT_FORMATS - def calculate_hash(octets: bytes, hash_algo: hashes.HashAlgorithm) -> bytes: h = hashes.Hash(hash_algo) @@ -20,120 +10,3 @@ def calculate_hash(octets: bytes, hash_algo: hashes.HashAlgorithm) -> bytes: def calculate_sha1_hash(octets: bytes) -> bytes: return calculate_hash(octets, hashes.SHA1()) - - -def argparse_enum_type_parser(enum_type): - def _parse(value): - value = value.upper() - - try: - return enum_type[value] - except KeyError: - raise ValueError(value) - - return _parse - - -class SeverityThresholdAction(argparse.Action): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - if values == 'ALL': - severity = None - elif values not in [s.name for s in validation.ValidationFindingSeverity]: - raise argparse.ArgumentError(self, f'Invalid severity value: "{values}"') - else: - severity = validation.ValidationFindingSeverity[values] - - setattr(namespace, self.dest, severity) - - -def add_severity_arg(parser): - parser.add_argument('-s', '--severity', - type=str.upper, - default=validation.ValidationFindingSeverity.INFO, - help='The finding severity threshold; findings with a lesser severity will not be reported.', - action=SeverityThresholdAction, - choices=[s.name for s in validation.ValidationFindingSeverity] + ['ALL'] - ) - - -class ReportFormatAction(argparse.Action): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - report_generator_cls = REPORT_FORMATS[values] - - setattr(namespace, self.dest, functools.partial(report_wrapper, report_generator_cls)) - - -def add_report_format_arg(parser): - parser.add_argument('-f', '--format', - type=str.upper, - default=functools.partial(report_wrapper, report.ReportGeneratorPlaintext), - help='The format in which results will be output.', - choices=list(REPORT_FORMATS.keys()), - action=ReportFormatAction - ) - - -class ValidityPeriodStartAction(argparse.Action): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @classmethod - def get_retriever_class(cls) -> Type[document.ValidityPeriodStartRetriever]: - pass - - def __call__(self, parser, namespace, values, option_string=None): - value_casefolded = values.casefold() - - if value_casefolded == 'document'.casefold(): - retriever_instance = self.get_retriever_class()() - elif value_casefolded == 'now'.casefold(): - retriever_instance = document.StaticValidityPeriodStartRetriever(datetime.datetime.now(tz=datetime.timezone.utc)) - else: - try: - dt = dateutil.parser.isoparse(values) - - retriever_instance = document.StaticValidityPeriodStartRetriever(dt) - except ValueError as e: - raise argparse.ArgumentError(self, f'Invalid value for validity period start: "{values}"') - - setattr(namespace, self.dest, retriever_instance) - - -class CertificateValidityPeriodStartAction(ValidityPeriodStartAction): - @classmethod - def get_retriever_class(cls): - return certificate_validity.CertificateValidityPeriodStartRetriever - - -def add_validity_period_start_arg(action_cls: Type[ValidityPeriodStartAction], parser): - parser.add_argument( - '--validity-period-start', - action=action_cls, - default=action_cls.get_retriever_class()(), - help='The start of the validity period that is compared to effective dates to determine applicability of ' - 'validations. Acceptable values are "DOCUMENT" (use the validity period indicated in the document being ' - 'validated, "NOW" (use the current time), or an ISO 8601-formatted date/time value.' - ) - - -add_certificate_validity_period_start_arg = functools.partial( - add_validity_period_start_arg, - CertificateValidityPeriodStartAction -) - - -def add_standard_args(parser): - add_severity_arg(parser) - add_report_format_arg(parser) - - -# This ensures that if a large (>255) number of findings are reported, we don't accidentally exit with an -# exit code of 0. This could happen if the number of findings is a multiple of 256. -def clamp_exit_code(exit_code): - return min(exit_code, 255) diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index c7fb0c0..003c4fe 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -498,3 +498,48 @@ def test_exit_code_multiple_256_findings(): def test_lint_etsi_cert_validations(): for cert_type in etsi_constants.CertificateType: _test_program_validations('lint_etsi_cert', ['-t', cert_type.to_option_str]) + + +def test_lint_pkix_der_only(): + ret = subprocess.run( + ['lint_pkix_cert', 'lint', '--document-format', 'der', '-'], + input=b"""-----BEGIN CERTIFICATE----- +MIIGgDCCBWigAwIBAgIQNr2Tbdy6bU+VjrmujHpQNDANBgkqhkiG9w0BAQsFADBv +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xRzBFBgNVBAMT +PkRpZ2lDZXJ0IFBLSSBQbGF0Zm9ybSBDMiBTaGFyZWQgU01JTUUgSW5kaXZpZHVh +bCBTdWJzY3JpYmVyIENBMB4XDTIzMDMwOTAwMDAwMFoXDTI1MDMwODIzNTk1OVow +MDEWMBQGA1UEAwwNQ29yZXkgQm9ubmVsbDEWMBQGA1UECgwNRGlnaWNlcnQsIElu +YzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4WdbzTHQqjkTCru6XZ +nQ7UYS6Mydr+uXb0tlSVdadFj+m8eUv9G437Hbv6VAJxTl2PN+gHTsp5WYAX2QC2 +EnfZ+98d4HxsX4/AxB9HXRyfrsuY28k2sQYl/ltPQyAJlI6DMvfj9DtjYkS6kesi +1TLI0IbqV4aw1YrydxOwt51EoSUJdFx4a6FSWSFERjcXp/FVKMruQxGClzRhkgOr +bwD7IVezqRsO+Lu4Skoraf5q7U2aW3BSAcTz9CN/xpI/eJ0gEECjQ21Qk2UYVWi4 +R2PyQiDp357vTwdYD1QMKPONN+IGCValRtP+T/W0rZ8dZfMXKBHcrWv1J2sbyfbQ +JS8CAwEAAaOCA1UwggNRMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBYG +A1UdJQEB/wQMMAoGCCsGAQUFBwMEMCUGA1UdEQQeMByBGkNvcmV5LkJvbm5lbGxA +ZGlnaWNlcnQuY29tMIIBIgYDVR0gBIIBGTCCARUwggERBglghkgBhv1sBQIwggEC +MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIHVBggr +BgEFBQcCAjCByBqBxUFueSB1c2Ugb2YgdGhpcyBDZXJ0aWZpY2F0ZSBjb25zdGl0 +dXRlcyBhY2NlcHRhbmNlIG9mIHRoZSBEaWdpQ2VydCBDUC9DUFMgYW5kIFJlbHlp +bmcgUGFydHkgQWdyZWVtZW50IHdoaWNoIGxpbWl0IGxpYWJpbGl0eSBhbmQgYXJl +IGluY29ycG9yYXRlZCBoZXJlaW4gYnkgcmVmZXJlbmNlLiBodHRwczovL3d3dy5k +aWdpY2VydC5jb20vcnBhLXVhMF0GA1UdHwRWMFQwUqBQoE6GTGh0dHA6Ly9wa2kt +Y3JsLnN5bWF1dGguY29tL2NhXzRiNWQ1ZmQzYjI2NTFiMzUyMjkwZTM2NDZhYmNj +MDAxL0xhdGVzdENSTC5jcmwwfwYIKwYBBQUHAQEEczBxMCgGCCsGAQUFBzABhhxo +dHRwOi8vcGtpLW9jc3AuZGlnaWNlcnQuY29tMEUGCCsGAQUFBzAChjlodHRwOi8v +Y2FjZXIuc3ltYXV0aC5jb20vbXBraS9kaWdpY2VydGMyc2hhcmVkc21pbWVjYS5j +cnQwHwYDVR0jBBgwFoAU3LcfIDF0S5Qadq2Dgq34xqPwRF8wQgYJKoZIhvcNAQkP +BDUwMzAKBggqhkiG9w0DBzALBglghkgBZQMEAQIwCwYJYIZIAWUDBAEWMAsGCWCG +SAFlAwQBKjAtBgpghkgBhvhFARADBB8wHQYTYIZIAYb4RQEQAQICAQGEy9uOSBYG +OTUyMjY4MDkGCmCGSAGG+EUBEAUEKzApAgEAFiRhSFIwY0hNNkx5OXdhMmt0Y21F +dWMzbHRZWFYwYUM1amIyMD0wHQYDVR0OBBYEFF5NZpSDXnDH25XcoXsZvqFS2BBN +MA0GCSqGSIb3DQEBCwUAA4IBAQCQHNrg9EHhTvBJ5drm99rxZCmCQx5AnjuDasDU +XUtRKqy/v1wT8nkNjVceIyzvF6EOd3PPtGJfum+oRe97eRkAk2nlpLL8//vO7GWU +a7lofBAJW1ETVvDVECAoqcdkPHxQM22caTGlJGrd6QGAzMoOAFTDSDhqT3ceiKU4 +rdKbtaTErZf73ZWonFxFdz49cJ6AC46NVJPiZmAEAqQVc14q6W4/w9SpWIpxcj6d +vx/vVMi1ilVWDucJYogvEic8X3uCfYBPHTwPHEKvvnXAoJMTTVnJM5CKxVrp09QS +6vmg7EN5ZeFVnjID0GzhfxWBR5/scJCF/s3DGuI0uCCtAruW +-----END CERTIFICATE-----""" + ) + + assert ret.returncode == 1 diff --git a/tests/test_loader.py b/tests/test_loader.py index f8af1df..3a9dfde 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -263,15 +263,15 @@ def _test_loader_obj(loader_instance, doc_b64): def test_certificate_loader(): - _test_loader_obj(loader._RFC5280_CERTIFICATE_LOADER, _CERT_B64) + _test_loader_obj(loader._RFC5280_CERTIFICATE_LOADER_INSTANCE, _CERT_B64) def test_crl_loader(): - _test_loader_obj(loader._RFC5280_CERTIFICATE_LIST_LOADER, _CRL_B64) + _test_loader_obj(loader._RFC5280_CERTIFICATE_LIST_LOADER_INSTANCE, _CRL_B64) def test_ocsp_response_loader(): - _test_loader_obj(loader._RFC6960_OCSP_RESPONSE_LOADER, _OCSP_RESPONSE_B64) + _test_loader_obj(loader._RFC6960_OCSP_RESPONSE_LOADER_INSTANCE, _OCSP_RESPONSE_B64) def test_load_cert_with_trailer():