diff --git a/src/cryptography/hazmat/_oid.py b/src/cryptography/hazmat/_oid.py index fd5e37d9e2ff..3d5f8e4773ca 100644 --- a/src/cryptography/hazmat/_oid.py +++ b/src/cryptography/hazmat/_oid.py @@ -14,6 +14,7 @@ class ExtensionOID: SUBJECT_DIRECTORY_ATTRIBUTES = ObjectIdentifier("2.5.29.9") SUBJECT_KEY_IDENTIFIER = ObjectIdentifier("2.5.29.14") KEY_USAGE = ObjectIdentifier("2.5.29.15") + PRIVATE_KEY_USAGE_PERIOD = ObjectIdentifier("2.5.29.16") SUBJECT_ALTERNATIVE_NAME = ObjectIdentifier("2.5.29.17") ISSUER_ALTERNATIVE_NAME = ObjectIdentifier("2.5.29.18") BASIC_CONSTRAINTS = ObjectIdentifier("2.5.29.19") @@ -273,6 +274,7 @@ class AttributeOID: ExtensionOID.SUBJECT_DIRECTORY_ATTRIBUTES: "subjectDirectoryAttributes", ExtensionOID.SUBJECT_KEY_IDENTIFIER: "subjectKeyIdentifier", ExtensionOID.KEY_USAGE: "keyUsage", + ExtensionOID.PRIVATE_KEY_USAGE_PERIOD: "privateKeyUsagePeriod", ExtensionOID.SUBJECT_ALTERNATIVE_NAME: "subjectAltName", ExtensionOID.ISSUER_ALTERNATIVE_NAME: "issuerAltName", ExtensionOID.BASIC_CONSTRAINTS: "basicConstraints", diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py index 26c6444c511f..f88915edb491 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -63,6 +63,7 @@ PolicyInformation, PrecertificateSignedCertificateTimestamps, PrecertPoison, + PrivateKeyUsagePeriod, ReasonFlags, SignedCertificateTimestamps, SubjectAlternativeName, @@ -111,6 +112,7 @@ OID_INHIBIT_ANY_POLICY = ExtensionOID.INHIBIT_ANY_POLICY OID_ISSUER_ALTERNATIVE_NAME = ExtensionOID.ISSUER_ALTERNATIVE_NAME OID_KEY_USAGE = ExtensionOID.KEY_USAGE +OID_PRIVATE_KEY_USAGE_PERIOD = ExtensionOID.PRIVATE_KEY_USAGE_PERIOD OID_NAME_CONSTRAINTS = ExtensionOID.NAME_CONSTRAINTS OID_OCSP_NO_CHECK = ExtensionOID.OCSP_NO_CHECK OID_POLICY_CONSTRAINTS = ExtensionOID.POLICY_CONSTRAINTS @@ -226,6 +228,7 @@ "PolicyInformation", "PrecertPoison", "PrecertificateSignedCertificateTimestamps", + "PrivateKeyUsagePeriod", "PublicKeyAlgorithmOID", "RFC822Name", "ReasonFlags", diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index 5e7486a594ed..26c66e5365f4 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -1275,6 +1275,68 @@ def public_bytes(self) -> bytes: return rust_x509.encode_extension_value(self) +class PrivateKeyUsagePeriod(ExtensionType): + oid = ExtensionOID.PRIVATE_KEY_USAGE_PERIOD + + def __init__( + self, + not_before: datetime.datetime | None = None, + not_after: datetime.datetime | None = None, + ) -> None: + if not isinstance(not_before, datetime.datetime) and not_before is not None: + raise TypeError( + "not_before must be a datetime.datetime or None" + ) + + if not isinstance(not_after, datetime.datetime) and not_after is not None: + raise TypeError( + "not_after must be a datetime.datetime or None" + ) + + if not_before is None and not_after is None: + raise ValueError( + "At least one of not_before and not_after must not be None" + ) + + if not_before is not None and not_after is not None and not_before > not_after: + raise ValueError( + "not_before must be before not_after" + ) + + self._not_before = not_before + self._not_after = not_after + + @property + def not_before(self) -> datetime.datetime | None: + return self._not_before + + @property + def not_after(self) -> datetime.datetime | None: + return self._not_after + + def __repr__(self) -> str: + return ( + f"" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PrivateKeyUsagePeriod): + return NotImplemented + + return self.not_before == other.not_before and self.not_after == other.not_after + + def __hash__(self) -> int: + return hash( + ( + self.not_before, + self.not_after + ) + ) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + class NameConstraints(ExtensionType): oid = ExtensionOID.NAME_CONSTRAINTS diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index 2fb5d5af272e..676c7fdb9cb1 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -12,7 +12,7 @@ use cryptography_x509::extensions::{ DistributionPointName, DuplicateExtensionsError, IssuerAlternativeName, KeyUsage, MSCertificateTemplate, NameConstraints, PolicyConstraints, PolicyInformation, PolicyQualifierInfo, Qualifier, RawExtensions, SequenceOfAccessDescriptions, - SequenceOfSubtrees, UserNotice, + SequenceOfSubtrees, UserNotice }; use cryptography_x509::extensions::{Extension, SubjectAlternativeName}; use cryptography_x509::{common, oid}; diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index a4368833ca3f..7e835ed7c3ec 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -4250,6 +4250,10 @@ def test_build_cert_with_rsa_key_too_small( encipher_only=False, decipher_only=False, ), + x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2002, 1, 1, 12, 1), + not_after=datetime.datetime(2030, 12, 31, 8, 30), + ), x509.OCSPNoCheck(), x509.SubjectKeyIdentifier, ], diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py index d11225fb3077..16913201749b 100644 --- a/tests/x509/test_x509_ext.py +++ b/tests/x509/test_x509_ext.py @@ -1876,6 +1876,65 @@ def test_key_cert_sign_crl_sign(self, backend): assert ku.crl_sign is True +class TestPrivateKeyUsagePeriodExtension: + def test_not_validity(self): + with pytest.raises(TypeError): + x509.PrivateKeyUsagePeriod("notvalidity") # type:ignore[arg-type] + + def test_repr(self): + period = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + ext = x509.Extension( + ExtensionOID.PRIVATE_KEY_USAGE_PERIOD, False, period + ) + assert repr(ext) == ( + ", " + "critical=False, value=)>" + ) + + def test_eq(self): + period = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + period2 = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + assert period == period2 + + def test_ne(self): + period = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + period2 = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2014, 1, 1), + ) + assert period != period2 + assert period != object() + + def test_hash(self): + period = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + period2 = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2013, 1, 1), + ) + period3 = x509.PrivateKeyUsagePeriod( + not_before=datetime.datetime(2012, 1, 1), + not_after=datetime.datetime(2014, 1, 1), + ) + assert hash(period) == hash(period2) + assert hash(period) != hash(period3) + + class TestDNSName: def test_non_a_label(self): with pytest.raises(ValueError): @@ -6320,6 +6379,7 @@ def test_all_extension_oid_members_have_names_defined(): for oid in dir(ExtensionOID): if oid.startswith("__"): continue + print(getattr(ExtensionOID, oid)) assert getattr(ExtensionOID, oid) in _OID_NAMES