Skip to content
Open
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
39 changes: 30 additions & 9 deletions localca_project/LocalCA/ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

import logging
import ipaddress
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
Expand All @@ -33,7 +34,7 @@ def __init__(self):
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False)
decipher_only=False,)

def generate_private_key(self):
"""
Expand Down Expand Up @@ -83,9 +84,9 @@ def create_root_certificate(
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
datetime.now(timezone.utc)
).not_valid_after(
datetime.utcnow() + timedelta(days=validity_days)
datetime.now(timezone.utc) + timedelta(days=validity_days)
).add_extension(
# Subject Key Identifier for this certificate
ski,
Expand Down Expand Up @@ -149,6 +150,8 @@ def create_leaf_certificate(
common_name: str,
san_list: list,
validity_days: int,
server_auth: bool,
client_auth: bool,
intermediate_public_key: str,
intermediate_private_key: str) -> dict:
"""
Expand All @@ -166,6 +169,8 @@ def create_leaf_certificate(
cert = self.sign_leaf_csr(
csr,
validity_days,
server_auth,
client_auth,
intermediate_cert,
intermediate_private_key)

Expand Down Expand Up @@ -278,9 +283,9 @@ def sign_csr(
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
datetime.now(timezone.utc)
).not_valid_after(
datetime.utcnow() + timedelta(days=validity_days)
datetime.now(timezone.utc) + timedelta(days=validity_days)
).add_extension(
# Subject Key Identifier for the issued certificate
ski,
Expand All @@ -306,6 +311,8 @@ def sign_leaf_csr(
self,
csr: x509.CertificateSigningRequest,
validity_days: int,
server_auth: bool,
client_auth: bool,
ca_cert: x509.Certificate,
ca_private_key: str) -> x509.Certificate:
"""
Expand All @@ -328,9 +335,9 @@ def sign_leaf_csr(
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
datetime.now(timezone.utc)
).not_valid_after(
datetime.utcnow() + timedelta(days=validity_days)
datetime.now(timezone.utc) + timedelta(days=validity_days)
)

# Add SAN extension if present in the CSR
Expand Down Expand Up @@ -360,6 +367,20 @@ def sign_leaf_csr(
path_length=None),
critical=True)

# Add server and client auth extensions if present
extended_key_usage = []
if server_auth:
extended_key_usage.append(x509.ExtendedKeyUsageOID.SERVER_AUTH)

if client_auth:
extended_key_usage.append(x509.ExtendedKeyUsageOID.CLIENT_AUTH)

if len(extended_key_usage) > 0:
builder = builder.add_extension(
x509.ExtendedKeyUsage(extended_key_usage),
critical=False
)

return builder.sign(
ca_private_key_obj,
hashes.SHA256(),
Expand Down
14 changes: 14 additions & 0 deletions localca_project/LocalCA/templates/LocalCA/create_leaf.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ <h4 class="mb-0">Create Leaf Certificate</h4>
<div class="form-text">Number of days the certificate will be valid (max 825 days)</div>
</div>

<!-- Usage -->
<div class="mb-4">
<label for="usage" class="form-label fw-bold">Key Usage</label>
<br>
<input type="checkbox"
id="server_auth"
name="server_auth"> Server Auth
<br>
<input type="checkbox"
id="client_auth"
name="client_auth"> Client Auth
<div class="form-text">Can be used to limit possible uses. Increasingly required by browsers and services.</div>
</div>

<div class="d-grid gap-2">
<button type="submit" class="btn btn-success btn-lg" {% if not intermediates %}disabled{% endif %}>
Create Leaf Certificate
Expand Down
93 changes: 93 additions & 0 deletions localca_project/LocalCA/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
This module contains the tests for the CertificateAuthority class.
'''
from datetime import datetime, timedelta

from cryptography.hazmat._oid import ExtendedKeyUsageOID
from cryptography.x509 import ExtensionNotFound
from django.test import TestCase
from django.utils import timezone
from django.urls import reverse
Expand Down Expand Up @@ -226,6 +229,8 @@ def test_create_leaf_certificate(self):
common_name="test.example.com",
san_list=san_list,
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand Down Expand Up @@ -277,6 +282,8 @@ def test_leaf_certificate_chain(self):
common_name="test.example.com",
san_list=["test.example.com"],
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand Down Expand Up @@ -307,6 +314,8 @@ def test_leaf_certificate_constraints(self):
common_name="test.example.com",
san_list=["test.example.com"],
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand All @@ -318,6 +327,84 @@ def test_leaf_certificate_constraints(self):
x509.BasicConstraints)
self.assertFalse(basic_constraints.value.ca)

def test_leaf_certificate_server_auth(self):
"""Test that leaf certificates cannot be CA certificates"""
root_cert = self.ca.create_root_certificate("Root CA", 365)
intermediate = self.ca.create_intermediate_certificate(
common_name="Intermediate CA",
validity_days=365,
root_public_key=root_cert["public_key"],
root_private_key=root_cert["private_key"]
)

leaf = self.ca.create_leaf_certificate(
common_name="test-server.example.com",
san_list=["test-server.example.com"],
validity_days=90,
server_auth=True,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)

# Load leaf certificate and check BasicConstraints
leaf_cert_obj = x509.load_pem_x509_certificate(
leaf["public_key"].encode())
extended_key_usages = leaf_cert_obj.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
self.assertEqual(extended_key_usages.value, x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]))

def test_leaf_certificate_client_auth(self):
"""Test that leaf certificates cannot be CA certificates"""
root_cert = self.ca.create_root_certificate("Root CA", 365)
intermediate = self.ca.create_intermediate_certificate(
common_name="Intermediate CA",
validity_days=365,
root_public_key=root_cert["public_key"],
root_private_key=root_cert["private_key"]
)

leaf = self.ca.create_leaf_certificate(
common_name="test-client.example.com",
san_list=["test-client.example.com"],
validity_days=90,
server_auth=False,
client_auth=True,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)

# Load leaf certificate and check auths
leaf_cert_obj = x509.load_pem_x509_certificate(
leaf["public_key"].encode())
extended_key_usages = leaf_cert_obj.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
self.assertEqual(extended_key_usages.value, x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]))

def test_leaf_certificate_client_server_auth(self):
"""Test that leaf certificates cannot be CA certificates"""
root_cert = self.ca.create_root_certificate("Root CA", 365)
intermediate = self.ca.create_intermediate_certificate(
common_name="Intermediate CA",
validity_days=365,
root_public_key=root_cert["public_key"],
root_private_key=root_cert["private_key"]
)

leaf = self.ca.create_leaf_certificate(
common_name="test-client-server.example.com",
san_list=["test-client-server.example.com"],
validity_days=90,
server_auth=True,
client_auth=True,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)

# Load leaf certificate and check auths
leaf_cert_obj = x509.load_pem_x509_certificate(
leaf["public_key"].encode())
extended_key_usages = leaf_cert_obj.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
self.assertEqual(extended_key_usages.value, x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CLIENT_AUTH]))

def test_create_leaf_certificate_with_invalid_inputs(self):
"""Test leaf certificate creation with invalid inputs"""
root_cert = self.ca.create_root_certificate("Root CA", 365)
Expand All @@ -334,6 +421,8 @@ def test_create_leaf_certificate_with_invalid_inputs(self):
common_name="",
san_list=["test.example.com"],
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand All @@ -360,6 +449,8 @@ def test_leaf_certificate_san_types(self):
common_name="test.example.com",
san_list=san_list,
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand Down Expand Up @@ -394,6 +485,8 @@ def test_create_pkcs12(self):
common_name="test.example.com",
san_list=["test.example.com"],
validity_days=90,
server_auth=False,
client_auth=False,
intermediate_public_key=intermediate["public_key"],
intermediate_private_key=intermediate["private_key"]
)
Expand Down
6 changes: 5 additions & 1 deletion localca_project/LocalCA/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ def create_leaf(request):
san_input = request.POST.get("san", "").strip()
validity_days = int(request.POST.get("validity_days"))
intermediate_id = int(request.POST.get("intermediate_id"))
server_auth = bool(request.POST.get("server_auth"))
client_auth = bool(request.POST.get("client_auth"))

# Process SANs
san_list = [san.strip()
Expand All @@ -332,6 +334,8 @@ def create_leaf(request):
common_name=common_name,
san_list=san_list, # Pass the complete list of SANs
validity_days=validity_days,
server_auth=server_auth,
client_auth=client_auth,
intermediate_public_key=intermediate_cert.public_key,
intermediate_private_key=intermediate_cert.private_key_encrypted,
)
Expand All @@ -345,7 +349,7 @@ def create_leaf(request):
public_key=leaf_cert_data["public_key"],
private_key_encrypted=leaf_cert_data["private_key"],
signed_by_intermediate=intermediate_cert,
created_at=datetime.utcnow(),
created_at=datetime.now(timezone.utc),
created_by=request.user,
)

Expand Down
Loading