Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Ed25519 certificates #3

Merged
merged 1 commit into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/certy/certificate_revocation_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 []
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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

Expand Down
28 changes: 20 additions & 8 deletions src/certy/credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -33,6 +33,8 @@ class KeyType(Enum):
"""Elliptic curve key (default)."""
RSA = 2
"""RSA key."""
ED25519 = 3
"""Ed25519 key."""


class KeyUsage(Enum):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -177,24 +179,26 @@ 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

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
Expand All @@ -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

Expand Down Expand Up @@ -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")

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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}")

Expand All @@ -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:
Expand All @@ -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}")
8 changes: 4 additions & 4 deletions tests/test_certificate_revocation_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.
#

from datetime import datetime
from datetime import datetime, timezone
import pytest
from cryptography import x509

Expand Down Expand Up @@ -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):
Expand Down
22 changes: 14 additions & 8 deletions tests/test_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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")
Expand All @@ -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():
Expand Down
2 changes: 2 additions & 0 deletions tests/test_credential_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading