diff --git a/src/certy/certificate_revocation_list.py b/src/certy/certificate_revocation_list.py index 17602c3..19c4f72 100644 --- a/src/certy/certificate_revocation_list.py +++ b/src/certy/certificate_revocation_list.py @@ -16,7 +16,7 @@ from __future__ import annotations -import datetime +from datetime import datetime, timedelta, timezone from cryptography import x509 from cryptography.hazmat.primitives import serialization @@ -31,8 +31,8 @@ def __init__( self, issuer: Credential | None = None, revoked_certificates: list[Credential] | None = None, - this_update: datetime.datetime | None = None, - next_update: datetime.datetime | None = None, + this_update: datetime | None = None, + next_update: datetime | None = None, ): self._issuer = issuer self._revoked_certificates = revoked_certificates or [] @@ -62,26 +62,26 @@ def issuer(self, issuer: Credential) -> CertificateRevocationList: self._issuer = issuer return self - def this_update(self, this_update: datetime.datetime) -> CertificateRevocationList: + def this_update(self, this_update: datetime) -> CertificateRevocationList: """Set the ``thisUpdate`` field of the CRL. If not called, the ``thisUpdate`` field will be set to the current time. :param this_update: The ``thisUpdate`` field of the CRL. - :type this_update: datetime.datetime + :type this_update: datetime :return: self :rtype: CertificateRevocationList """ self._this_update = this_update return self - def next_update(self, next_update: datetime.datetime) -> CertificateRevocationList: + def next_update(self, next_update: datetime) -> CertificateRevocationList: """Set the ``nextUpdate`` field of the CRL. If not called, the ``nextUpdate`` field will be set to ``thisUpdate`` plus 7 days. :param next_update: The nextUpdate field of the CRL. - :type next_update: datetime.datetime + :type next_update: datetime :return: self :rtype: CertificateRevocationList """ @@ -128,11 +128,11 @@ def generate(self) -> CertificateRevocationList: # Ensure that the issuer has a key pair. self._issuer._ensure_generated() - effective_revocation_time = datetime.datetime.utcnow() + effective_revocation_time = datetime.now(timezone.utc) if self._this_update: effective_revocation_time = self._this_update - effective_expiry_time = effective_revocation_time + datetime.timedelta(days=7) + effective_expiry_time = effective_revocation_time + timedelta(days=7) if self._next_update: effective_expiry_time = self._next_update diff --git a/src/certy/credential.py b/src/certy/credential.py index 865e6ea..f2f25f7 100644 --- a/src/certy/credential.py +++ b/src/certy/credential.py @@ -17,12 +17,12 @@ from __future__ import annotations import ipaddress -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from enum import Enum from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric import ec, rsa, ed25519 from cryptography.x509.oid import ExtendedKeyUsageOID @@ -33,6 +33,8 @@ class KeyType(Enum): """Elliptic curve key (default).""" RSA = 2 """RSA key.""" + ED25519 = 3 + """Ed25519 key.""" class KeyUsage(Enum): @@ -100,7 +102,7 @@ def __init__( self._crl_distribution_point_uri = crl_distribution_point_uri # Generated attributes - self._private_key: rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey | None = None + self._private_key: rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey | ed25519.Ed25519PrivateKey | None = None self._certificate: x509.Certificate | None = None def __repr__(self): @@ -177,13 +179,13 @@ def key_type(self, key_type: KeyType) -> Credential: If not called, the key type will be :const:`KeyType.EC`. - :param key_type: The key type of this credential. Must be :const:`KeyType.EC` or :const:`KeyType.RSA`. + :param key_type: The key type of this credential. Must be :const:`KeyType.EC`, :const:`KeyType.RSA` or :const:`KeyType.ED25519`. :type key_type: KeyType :return: This credential instance. :rtype: Credential """ if not isinstance(key_type, KeyType): - raise ValueError("Key type must be certy.KeyType.EC or certy.KeyType.RSA") + raise ValueError("Key type must be certy.KeyType.EC, certy.KeyType.RSA or certy.KeyType.ED25519") self._key_type = key_type return self @@ -191,10 +193,12 @@ def key_size(self, key_size: int) -> Credential: """Set the key size of this credential. If not called, the key size is ``256`` for :const:`KeyType.EC` and ``2048`` for :const:`KeyType.RSA`. + :const:`KeyType.ED25519` has a fixed key size of ``256``. :param key_size: The key size of this credential. Valid values depend on the key type. For EC keys, valid values are 256, 384, and 521. For RSA keys, valid values are 1024, 2048, and 4096. + For ED25519 keys, valid value is 256. :type key_size: int :return: This credential instance. :rtype: Credential @@ -205,6 +209,8 @@ def key_size(self, key_size: int) -> Credential: raise ValueError("EC key size must be 256, 384, or 521") elif self._key_type == KeyType.RSA and key_size < 1024: raise ValueError("RSA key size must be at least 1024") + elif self._key_type == KeyType.ED25519 and key_size != 256: + raise ValueError("ED25519 key size must be 256") self._key_size = key_size return self @@ -347,6 +353,8 @@ def generate(self) -> Credential: self._key_size = 256 elif self._key_type == KeyType.RSA: self._key_size = 2048 + elif self._key_type == KeyType.ED25519: + self._key_size = 256 else: raise ValueError("Unknown key type") @@ -372,7 +380,7 @@ def generate(self) -> Credential: effective_not_before = self._not_before effective_not_after = self._not_after if effective_not_before is None: - effective_not_before = datetime.utcnow() + effective_not_before = datetime.now(timezone.utc) if effective_not_after is None and self._expires is not None: effective_not_after = effective_not_before + self._expires elif effective_not_after is not None: @@ -586,7 +594,7 @@ def _get_chain(self) -> list[x509.Certificate]: # Helper functions -def _generate_new_key(key_type: KeyType, key_size: int) -> rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey: +def _generate_new_key(key_type: KeyType, key_size: int) -> rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey | ed25519.Ed25519PrivateKey: if key_type == KeyType.RSA: return rsa.generate_private_key(public_exponent=65537, key_size=key_size) elif key_type == KeyType.EC: @@ -601,6 +609,8 @@ def _generate_new_key(key_type: KeyType, key_size: int) -> rsa.RSAPrivateKey | e raise ValueError(f"Invalid key size: {key_size}") return ec.generate_private_key(curve) + elif key_type == KeyType.ED25519: + return ed25519.Ed25519PrivateKey.generate() raise ValueError(f"Invalid key type: {key_type}") @@ -620,7 +630,7 @@ def _as_general_names(names: list[str]) -> x509.GeneralNames: return x509.GeneralNames(general_names) -def _preferred_signature_hash_algorithm(key_type: KeyType, key_size: int) -> hashes.HashAlgorithm: +def _preferred_signature_hash_algorithm(key_type: KeyType, key_size: int) -> hashes.HashAlgorithm | None: if key_type == KeyType.RSA: return hashes.SHA256() elif key_type == KeyType.EC: @@ -632,5 +642,7 @@ def _preferred_signature_hash_algorithm(key_type: KeyType, key_size: int) -> has return hashes.SHA512() else: raise ValueError(f"Invalid key size: {key_size}") + elif key_type == KeyType.ED25519: + return None else: raise ValueError(f"Invalid key type: {key_type!r}") diff --git a/tests/test_certificate_revocation_list.py b/tests/test_certificate_revocation_list.py index 3b52a1a..0d338a4 100644 --- a/tests/test_certificate_revocation_list.py +++ b/tests/test_certificate_revocation_list.py @@ -14,7 +14,7 @@ # limitations under the License. # -from datetime import datetime +from datetime import datetime, timezone import pytest from cryptography import x509 @@ -43,14 +43,14 @@ def test_this_update(ca): crl = CertificateRevocationList().issuer(ca).this_update(datetime(2023, 10, 31, 9, 0)) got = x509.load_der_x509_crl(crl.get_as_der()) assert got is not None - assert got.last_update == datetime(2023, 10, 31, 9, 0) + assert got.last_update_utc == datetime(2023, 10, 31, 9, 0, tzinfo=timezone.utc) def test_next_update(ca): - crl = CertificateRevocationList().issuer(ca).next_update(datetime(2023, 10, 31, 9, 0)) + crl = CertificateRevocationList().issuer(ca).this_update(datetime(2023, 10, 31, 9, 0)).next_update(datetime(2024, 10, 31, 9, 0)) got = x509.load_der_x509_crl(crl.get_as_der()) assert got is not None - assert got.next_update == datetime(2023, 10, 31, 9, 0) + assert got.next_update_utc == datetime(2024, 10, 31, 9, 0, tzinfo=timezone.utc) def test_issuer(ca): diff --git a/tests/test_credential.py b/tests/test_credential.py index eb79423..239ff74 100644 --- a/tests/test_credential.py +++ b/tests/test_credential.py @@ -16,12 +16,12 @@ import datetime import ipaddress -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from cryptography import x509 from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric import ec, rsa, ed25519 from cryptography.x509.oid import ExtendedKeyUsageOID from certy import Credential, ExtendedKeyUsage, KeyType, KeyUsage @@ -83,10 +83,16 @@ def test_rsa_key_sizes(): assert key_must_be(cred, rsa.RSAPrivateKey, 4096) +def test_ed25519_certificate(): + # Ed25519 has fixed key size, so key_size() should not be used. + cred = Credential().subject("CN=test").key_type(KeyType.ED25519).generate() + isinstance(cred.get_private_key(), ed25519.Ed25519PrivateKey) + + def test_expires(): cred = Credential().subject("CN=test").expires(timedelta(days=365)).generate() cert = cred.get_certificate() - assert cert.not_valid_after - cert.not_valid_before == timedelta(days=365) + assert cert.not_valid_after_utc - cert.not_valid_before_utc == timedelta(days=365) def test_key_usages(): @@ -198,8 +204,8 @@ def test_intermediate_ca(): def test_not_before_and_not_after(): - want_not_before = datetime(2023, 1, 1, 0, 0, 0) - want_not_after = datetime(2023, 1, 2, 0, 0, 0) + want_not_before = datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + want_not_after = datetime(2023, 1, 2, 0, 0, 0, tzinfo=timezone.utc) cert = ( Credential() .subject("CN=joe") @@ -208,12 +214,12 @@ def test_not_before_and_not_after(): .generate() .get_certificate() ) - assert cert.not_valid_before == want_not_before - assert cert.not_valid_after == want_not_after + assert cert.not_valid_before_utc == want_not_before + assert cert.not_valid_after_utc == want_not_after expires = timedelta(days=365) cert = Credential().subject("CN=joe").expires(expires).generate().get_certificate() - assert cert.not_valid_after - cert.not_valid_before == expires + assert cert.not_valid_after_utc - cert.not_valid_before_utc == expires def test_serial_number(): diff --git a/tests/test_credential_errors.py b/tests/test_credential_errors.py index 5c9ff58..18cd157 100644 --- a/tests/test_credential_errors.py +++ b/tests/test_credential_errors.py @@ -38,6 +38,8 @@ def test_invalid_key_size(): Credential().subject("CN=joe").key_type(KeyType.RSA).key_size(123) with pytest.raises(ValueError): Credential().subject("CN=joe").key_size("not a valid key size") + with pytest.raises(ValueError): + Credential().subject("CN=joe").key_type(KeyType.ED25519).key_size(123) def test_invalid_ca():