diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d7fe6..9599891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ All notable changes to this project from version 0.9.3 onwards are documented in this file. -## 0.11.4 - 2024-08-28 +## 0.11.4 - 2024-08-29 ### New features/enhancements - Use pyasn1-fasder for ASN.1 DER decoding by default (#98) +- Add support for S/MIME working group ballot SMC-08 (#101) ## 0.11.3 - 2024-07-17 diff --git a/README.md b/README.md index 36e34be..db73676 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,13 @@ For example, the following mapping file is used to map policy OID `1.2.3.4.5.6` The `-o`/`--output` option is used to specify that the validation level and generation used by the linter is written to standard error. This is useful when using the `--guess` option to see which validation level and generation was determined by the heuristics logic. +The `--validity-period-start` option is used to override how the issuance date/time of a certificate is determined. Many requirements are applicable based on the date/time of issuance of certificates, so this option is useful +to evaluate whether a certificate complies with an upcoming requirement. There are three possible types of values for this option: + +1. `DOCUMENT`: Use the value of the `notBefore` field to determine the issuance date/time. This is the default value. +2. `NOW`: Use the current date/time to override the issuance date/time. +3. An ISO 8601 timestamp: Use the specified timestamp to override the issuance date/time. + #### Example command execution ```shell diff --git a/pkilint/bin/lint_cabf_smime_cert.py b/pkilint/bin/lint_cabf_smime_cert.py index 37721ef..930d8c0 100644 --- a/pkilint/bin/lint_cabf_smime_cert.py +++ b/pkilint/bin/lint_cabf_smime_cert.py @@ -93,6 +93,8 @@ 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) + util.add_standard_args(lint_parser) lint_parser.add_argument('file', @@ -142,7 +144,7 @@ def main(cli_args=None) -> int: doc_validator = certificate.create_pkix_certificate_validator_container( smime.create_decoding_validators(), smime.create_subscriber_validators( - validation_level, generation + validation_level, generation, args.validity_period_start ) ) diff --git a/pkilint/cabf/smime/__init__.py b/pkilint/cabf/smime/__init__.py index 5c0a4fd..a16f530 100644 --- a/pkilint/cabf/smime/__init__.py +++ b/pkilint/cabf/smime/__init__.py @@ -1,5 +1,5 @@ import operator -from typing import Mapping, Tuple +from typing import Mapping, Tuple, Optional from dateutil.relativedelta import relativedelta from pyasn1.type import univ @@ -15,7 +15,7 @@ from pkilint.adobe import adobe_validator from pkilint.cabf import cabf_extension, cabf_key, cabf_name from pkilint.cabf.smime import ( - smime_constants, smime_name, smime_key, smime_extension + smime_constants, smime_name, smime_key, smime_extension, smime_validity ) from pkilint.cabf.smime.smime_constants import Generation from pkilint.common import alternative_name @@ -23,6 +23,7 @@ from pkilint.msft import asn1 as microsoft_asn1 from pkilint.msft import msft_name from pkilint.pkix import certificate, time +from pkilint.pkix.certificate import certificate_validity from pkilint.pkix.general_name import OTHER_NAME_MAPPINGS as PKIX_OTHERNAME_MAPPINGS OTHER_NAME_MAPPINGS = { @@ -192,7 +193,7 @@ def create_extensions_validator_container(validation_level, generation): ) -def create_validity_validators(generation): +def create_validity_validators(generation, validity_period_start_retriever: document.ValidityPeriodStartRetriever): days = 1185 if generation == Generation.LEGACY else 825 threshold_error = ( @@ -212,7 +213,8 @@ def create_validity_validators(generation): 'cabf.smime.certificate_validity_period_at_maximum' ) ) - return [ + + validators = [ time.ValidityPeriodThresholdsValidator( path='certificate.tbsCertificate.validity.notBefore', end_validity_node_retriever=lambda n: n.navigate('^.notAfter'), @@ -221,8 +223,20 @@ def create_validity_validators(generation): ) ] + if generation == smime_constants.Generation.LEGACY: + validators.append(smime_validity.LegacyGenerationSunsetValidator(validity_period_start_retriever)) + + return validators + + +def create_subscriber_validators( + validation_level, + generation, + validity_period_start_retriever: Optional[document.ValidityPeriodStartRetriever] = None +): + if validity_period_start_retriever is None: + validity_period_start_retriever = certificate_validity.CertificateValidityPeriodStartRetriever() -def create_subscriber_validators(validation_level, generation): return [ smime_name.create_subscriber_certificate_subject_validator_container(validation_level, generation), create_spki_validation_container(), @@ -230,7 +244,7 @@ def create_subscriber_validators(validation_level, generation): [] ), certificate.create_validity_validator_container( - create_validity_validators(generation) + create_validity_validators(generation, validity_period_start_retriever) ), create_extensions_validator_container(validation_level, generation), smime_key.SmimeAllowedSignatureAlgorithmEncodingValidator( diff --git a/pkilint/cabf/smime/finding_metadata.csv b/pkilint/cabf/smime/finding_metadata.csv index 23b3862..990367f 100644 --- a/pkilint/cabf/smime/finding_metadata.csv +++ b/pkilint/cabf/smime/finding_metadata.csv @@ -37,6 +37,7 @@ ERROR,cabf.smime.extended_key_usage_extension_missing,SMBR 7.1.2.3 (f),"""SHALL 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) ERROR,cabf.smime.is_ca_certificate,SMBR 7.1.2.3 (d),"""The cA field SHALL NOT be true""" ERROR,cabf.smime.key_usage_extension_missing,SMBR 7.1.2.3 (e),"""SHALL be present""" +ERROR,cabf.smime.legacy_generation_certificate_issued_after_prohibition,SMBR 7.1.6.1,"Effective July 15, 2025 S/MIME Subscriber Certificates SHALL NOT be issued using the Legacy Generation profiles 2.23.140.1.5.1.1, 2.23.140.1.5.2.1, 2.23.140.1.5.3.1, or 2.23.140.1.5.4.1." ERROR,cabf.smime.lei_extension_critical,SMBR 7.1.2.3 (l),""" SHALL NOT be marked critical""" ERROR,cabf.smime.lei_extension_prohibited,SMBR 7.1.2.3 (l),Mailbox- and individual-validated: Prohibited ERROR,cabf.smime.lei_role_extension_critical,SMBR 7.1.2.3 (l),"""SHALL NOT be marked critical""" diff --git a/pkilint/cabf/smime/smime_constants.py b/pkilint/cabf/smime/smime_constants.py index fd9b906..730cf78 100644 --- a/pkilint/cabf/smime/smime_constants.py +++ b/pkilint/cabf/smime/smime_constants.py @@ -5,7 +5,7 @@ from pyasn1.type.univ import ObjectIdentifier -BR_VERSION = '1.0.4' +BR_VERSION = '1.0.6' CABF_SMIME_OID_ARC = ObjectIdentifier('2.23.140.1.5') diff --git a/pkilint/cabf/smime/smime_validity.py b/pkilint/cabf/smime/smime_validity.py new file mode 100644 index 0000000..41c7f19 --- /dev/null +++ b/pkilint/cabf/smime/smime_validity.py @@ -0,0 +1,28 @@ +import datetime + +from pyasn1_alt_modules import rfc5280 + +from pkilint import validation, document + + +class LegacyGenerationSunsetValidator(validation.Validator): + VALIDATION_LEGACY_GENERATION_CERTIFICATE_ISSUED_AFTER_PROHIBITION = validation.ValidationFinding( + validation.ValidationFindingSeverity.ERROR, + 'cabf.smime.legacy_generation_certificate_issued_after_prohibition' + ) + + _LEGACY_GENERATION_SUNSET_DATE = datetime.datetime(2025, 7, 15, 0, 0, 0, tzinfo=datetime.timezone.utc) + + def __init__(self, validity_period_start_retriever: document.ValidityPeriodStartRetriever): + super().__init__( + validations=[self.VALIDATION_LEGACY_GENERATION_CERTIFICATE_ISSUED_AFTER_PROHIBITION], + pdu_class=rfc5280.Validity + ) + + self._validity_period_start_retriever = validity_period_start_retriever + + def validate(self, node): + if self._validity_period_start_retriever(node.document) >= self._LEGACY_GENERATION_SUNSET_DATE: + raise validation.ValidationFindingEncountered( + self.VALIDATION_LEGACY_GENERATION_CERTIFICATE_ISSUED_AFTER_PROHIBITION + ) diff --git a/tests/integration_certificate/smime_br/individual/legacy/issued_after_legacy_sunset.crttest b/tests/integration_certificate/smime_br/individual/legacy/issued_after_legacy_sunset.crttest new file mode 100644 index 0000000..24ebf0e --- /dev/null +++ b/tests/integration_certificate/smime_br/individual/legacy/issued_after_legacy_sunset.crttest @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIF1DCCA7ygAwIBAgIUL745jJJYnvMFGPE42o7ukLDYP/IwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCVVMxHzAdBgNVBAoMFkZvbyBJbmR1c3RyaWVzIExpbWl0 +ZWQxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAeFw0yNjA0MjgwMDAwMDBaFw0y +NzA3MjcyMzU5NTlaMEIxFjAUBgNVBAMMDVlBTUFEQSBIYW5ha28xKDAmBgkqhkiG +9w0BCQEWGWhhbmFrby55YW1hZGFAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI ++1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f +qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 +yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe +NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 +JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjggG6MIIBtjAM +BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAfBgNVHSMEGDAWgBTWRAAyfKgN +/6xPa2buta6bLMU4VDAdBgNVHQ4EFgQUiRlZXg7xafXLvUfhNPzimMxpMJEwFAYD +VR0gBA0wCzAJBgdngQwBBQQBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwu +Y2EuZXhhbXBsZS5jb20vaXNzdWluZ19jYV9jcmwuY3JsMEsGCCsGAQUFBwEBBD8w +PTA7BggrBgEFBQcwAoYvaHR0cDovL3JlcG9zaXRvcnkuY2EuZXhhbXBsZS5jb20v +aXNzdWluZ19jYS5kZXIwHQYDVR0lBBYwFAYIKwYBBQUHAwQGCCsGAQUFBwMCMIGU +BgNVHREEgYwwgYmBGWhhbmFrby55YW1hZGFAZXhhbXBsZS5jb22gKQYKKwYBBAGC +NxQCA6AbDBloYW5ha28ueWFtYWRhQGV4YW1wbGUuY29toCYGCCsGAQUFBwgJoBoM +GOWxseeUsOiKseWtkEBleGFtcGxlLmNvbaQZMBcxFTATBgNVBAMMDOWxseeUsOiK +seWtkDANBgkqhkiG9w0BAQsFAAOCAgEAjEOEra5mkkzlUePGTbMa8KoXh7eo/xfx +8SzPuE6HF4O6kCmh6AD1bq0T2ahLYwniSOtE3iBg9KhauALzjqYL+ko0JkvmIRLN +BHGS3UNDMz5FW8+7+coU4MwlVuQUOyt9t01ZkbOBx5FkLupcONYcbAuY4gGPUEcu +kjyCQnIA6LjlqCtD9DhIjFr7f7DFsMXP2iNXBQs/TgZwxX+OIb9JIhkDhEPy0kou +5p7hzbJwnveA7cKxLUx9LuYBln65VB4iBdLjHRwbnc7LVOx9oZk/V9yNg/h3UjfB +L6MH6Kgac7o2dkdnH9yGTrhCkt9K09tzmi3ODv1bxjUbojzGDJbVC2YQ6HMntn88 +9S98J461+qYfZuxIoFp1HCXtJs1iZ2HPuz7sma+vUtd9s4S9OWGruyDkxZlClcZ+ +IgH7MZYnvByo3XZd+c3jk6692bqEYBgvu5tWrId4h9hVh7TY4pbKwwZnFojPLkHH +2OMkjJqK54R/PS3Ea0V8vInxwr8ESmLo/DZUuUNaX82UOkOXd2wwhHexlVtZM5Hs +IVDZxDulQS1lll+M6vAzHq9buxAdkRAiyC5X+dSqkP5itNCQBkJrnfi8tVfHg69X +APeclV11R1Ksnjn6OjwAKBmWnU99HzScksMx0SOI5G99+6mzz3zZLVQdPlnSj5V+ +tgkVdJ0YdSw= +-----END CERTIFICATE----- + +node_path,validator,severity,code,message +certificate.tbsCertificate.validity,LegacyGenerationSunsetValidator,ERROR,cabf.smime.legacy_generation_certificate_issued_after_prohibition, +certificate.tbsCertificate.extensions.3.extnValue.subjectKeyIdentifier,SubjectKeyIdentifierValidator,INFO,pkix.subject_key_identifier_method_1_identified,