Skip to content

Commit f83e3fd

Browse files
committed
Update scripts.
1 parent aa00e6b commit f83e3fd

1 file changed

Lines changed: 108 additions & 15 deletions

File tree

chaski/utils/certificate_authority.py

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import os
22
import ssl
33
import datetime
4+
from ipaddress import ip_address
45

56
# Importing cryptography modules for X509 certificates, private key generation,
67
# and serialization, including RSA key generation and PEM encoding without encryption.
78
from cryptography import x509
8-
from cryptography.x509.oid import NameOID
9+
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
910
from cryptography.hazmat.primitives import hashes, serialization
1011
from cryptography.hazmat.primitives.asymmetric import rsa
12+
from cryptography.hazmat.primitives.serialization import BestAvailableEncryption
13+
1114

1215
from chaski.utils import user_data_dir
1316

@@ -21,6 +24,8 @@ def __init__(
2124
ip_address: str,
2225
ssl_certificates_location: str = None,
2326
ssl_certificate_attributes: dict = {},
27+
key_password: bytes | None = None,
28+
end_entity_key_size: int = 4096,
2429
):
2530
"""
2631
Initialize the Certificate Authority (CA).
@@ -42,6 +47,8 @@ def __init__(
4247
os.makedirs(self.ssl_certificates_location, exist_ok=True)
4348
self.ssl_certificate_attributes = ssl_certificate_attributes
4449
self.ip_address = ip_address
50+
self.key_password = key_password
51+
self.end_entity_key_size = end_entity_key_size
4552

4653
def setup_certificate_authority(self) -> None:
4754
"""
@@ -65,18 +72,29 @@ def setup_certificate_authority(self) -> None:
6572
self.ca_cert_path_ = os.path.join(self.ssl_certificates_location, "ca.cert")
6673

6774
# Generate CA key
68-
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
75+
ca_key = rsa.generate_private_key(
76+
public_exponent=65537, key_size=self.end_entity_key_size
77+
)
78+
79+
# Write CA key to file
80+
encryption = (
81+
BestAvailableEncryption(self.key_password)
82+
if self.key_password
83+
else serialization.NoEncryption()
84+
)
6985

7086
# Write CA key to file
7187
with open(self.ca_key_path_, "wb") as f:
7288
f.write(
7389
ca_key.private_bytes(
7490
encoding=serialization.Encoding.PEM,
7591
format=serialization.PrivateFormat.TraditionalOpenSSL,
76-
encryption_algorithm=serialization.NoEncryption(),
92+
encryption_algorithm=encryption,
7793
)
7894
)
7995

96+
os.chmod(self.ca_key_path_, 0o600)
97+
8098
# Generate CA certificate
8199
subject = issuer = x509.Name(
82100
[
@@ -112,9 +130,9 @@ def setup_certificate_authority(self) -> None:
112130
.issuer_name(issuer)
113131
.public_key(ca_key.public_key())
114132
.serial_number(x509.random_serial_number())
115-
.not_valid_before(datetime.datetime.utcnow())
133+
.not_valid_before(datetime.datetime.now(datetime.UTC))
116134
.not_valid_after(
117-
datetime.datetime.utcnow() + datetime.timedelta(days=365 * 10)
135+
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365 * 10)
118136
)
119137
.add_extension(
120138
x509.BasicConstraints(ca=True, path_length=None),
@@ -148,6 +166,7 @@ def setup_certificate_authority(self) -> None:
148166
# Write CA certificate to file
149167
with open(self.ca_cert_path_, "wb") as f:
150168
f.write(ca_certificate.public_bytes(serialization.Encoding.PEM))
169+
os.chmod(self.ca_cert_path_, 0o644)
151170

152171
@property
153172
def ca_private_key_path(self) -> str:
@@ -222,7 +241,7 @@ def ca_certificate_path(self, path: str) -> None:
222241
"""
223242
self.ca_cert_path_ = path
224243

225-
def sign_csr(self, csr_data: bytes) -> bytes:
244+
def sign_csr(self, csr_data: bytes, is_server: bool | None = None) -> bytes:
226245
"""
227246
Sign a Certificate Signing Request (CSR) with the Certificate Authority (CA) key.
228247
@@ -233,6 +252,9 @@ def sign_csr(self, csr_data: bytes) -> bytes:
233252
----------
234253
csr_data : bytes
235254
The certificate signing request data in PEM format.
255+
is_server : bool, optional
256+
If True, indicates this is a server certificate. If False, indicates this is a client certificate.
257+
If None, will be inferred from the Common Name (CN) in the CSR by checking if it contains "server".
236258
237259
Returns
238260
-------
@@ -260,6 +282,28 @@ def sign_csr(self, csr_data: bytes) -> bytes:
260282
# Load client CSR
261283
csr = x509.load_pem_x509_csr(csr_data)
262284

285+
# infer by CN if not provided
286+
if is_server is None:
287+
cn = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
288+
is_server = "server" in cn.lower()
289+
290+
ku = x509.KeyUsage(
291+
digital_signature=True,
292+
content_commitment=False,
293+
key_encipherment=True,
294+
data_encipherment=False,
295+
key_agreement=False,
296+
key_cert_sign=False,
297+
crl_sign=False,
298+
encipher_only=False,
299+
decipher_only=False,
300+
)
301+
eku = x509.ExtendedKeyUsage(
302+
[ExtendedKeyUsageOID.SERVER_AUTH]
303+
if is_server
304+
else [ExtendedKeyUsageOID.CLIENT_AUTH]
305+
)
306+
263307
# Generate client certificate
264308
certificate = (
265309
x509.CertificateBuilder()
@@ -272,10 +316,10 @@ def sign_csr(self, csr_data: bytes) -> bytes:
272316
# Generate a random serial number for the certificate to ensure its uniqueness.
273317
.serial_number(x509.random_serial_number())
274318
.not_valid_before(
275-
datetime.datetime.utcnow()
319+
datetime.datetime.now(datetime.UTC)
276320
) # Set certificate's validity start time to current time
277321
.not_valid_after(
278-
datetime.datetime.utcnow()
322+
datetime.datetime.now(datetime.UTC)
279323
+ datetime.timedelta(
280324
days=365
281325
) # Set certificate's expiration time to one year from now
@@ -286,8 +330,16 @@ def sign_csr(self, csr_data: bytes) -> bytes:
286330
critical=True,
287331
)
288332
# Add the subject alternative name extension with the IP address
333+
.add_extension(ku, critical=True)
334+
.add_extension(eku, critical=False)
289335
.add_extension(
290-
x509.SubjectAlternativeName([x509.IPAddress(self.ip_address)]),
336+
(
337+
x509.SubjectAlternativeName(
338+
[x509.IPAddress(ip_address(self.ip_address))]
339+
)
340+
if is_server
341+
else x509.SubjectAlternativeName([])
342+
), # no SAN-IP en client por defecto
291343
critical=False,
292344
)
293345
.add_extension(
@@ -336,17 +388,27 @@ def _key_and_csr(self, name="client") -> tuple:
336388
self.ssl_certificates_location, f"{name}_{self.id}.csr"
337389
)
338390

339-
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
391+
key = rsa.generate_private_key(
392+
public_exponent=65537, key_size=self.end_entity_key_size
393+
)
394+
395+
# Choose encryption strategy for private key
396+
encryption = (
397+
BestAvailableEncryption(self.key_password)
398+
if self.key_password
399+
else serialization.NoEncryption()
400+
)
340401

341402
# Write client key to file
342403
with open(private_key_path_, "wb") as f:
343404
f.write(
344405
key.private_bytes(
345406
encoding=serialization.Encoding.PEM,
346407
format=serialization.PrivateFormat.TraditionalOpenSSL,
347-
encryption_algorithm=serialization.NoEncryption(),
408+
encryption_algorithm=encryption,
348409
)
349410
)
411+
os.chmod(private_key_path_, 0o600)
350412

351413
# Generate client CSR
352414
csr = (
@@ -383,6 +445,7 @@ def _key_and_csr(self, name="client") -> tuple:
383445
# Write client CSR to file
384446
with open(certificate_path_, "wb") as f:
385447
f.write(csr.public_bytes(serialization.Encoding.PEM))
448+
os.chmod(certificate_path_, 0o644)
386449

387450
return private_key_path_, certificate_path_
388451

@@ -446,7 +509,7 @@ def load_key_and_csr(
446509
self.certificate_server_path_ = certificate_server_path
447510

448511
@property
449-
def private_key_paths(self) -> str:
512+
def private_key_paths(self) -> dict:
450513
"""
451514
Return the path to the node's private key.
452515
@@ -473,7 +536,7 @@ def private_key_paths(self) -> str:
473536
raise Exception("Private key path not set")
474537

475538
@property
476-
def certificate_paths(self) -> str:
539+
def certificate_paths(self) -> dict:
477540
"""
478541
Retrieve the path to the node's Certificate Signing Request (CSR).
479542
@@ -500,7 +563,7 @@ def certificate_paths(self) -> str:
500563
raise Exception("CA certificate path not set")
501564

502565
@property
503-
def certificate_signed_paths(self) -> str:
566+
def certificate_signed_paths(self) -> dict:
504567
"""
505568
Provide the path to the signed certificate.
506569
@@ -574,7 +637,7 @@ def write_certificate(self, path: str, certificate: bytes) -> None:
574637
with open(path, "wb") as cert_file:
575638
cert_file.write(certificate)
576639

577-
def get_context(self) -> ssl.SSLContext:
640+
def get_context(self) -> tuple[ssl.SSLContext, ssl.SSLContext]:
578641
"""
579642
Create and configure an SSL context for secure communication.
580643
@@ -619,3 +682,33 @@ def get_context(self) -> ssl.SSLContext:
619682
ssl_context_server.verify_mode = ssl.CERT_REQUIRED
620683

621684
return ssl_context_client, ssl_context_server
685+
686+
def sign_and_store(self, name: str) -> str:
687+
"""
688+
Sign a Certificate Signing Request (CSR) and store the resulting certificate.
689+
690+
This method takes a CSR for either a client or server, signs it using the
691+
Certificate Authority's private key, and stores the resulting signed certificate.
692+
693+
Parameters
694+
----------
695+
name : str
696+
Name identifier for the certificate ('client' or 'server')
697+
698+
Returns
699+
-------
700+
str
701+
Path to the stored signed certificate file
702+
703+
Raises
704+
------
705+
KeyError
706+
If the provided name is not 'client' or 'server'
707+
IOError
708+
If there is an error reading the CSR or writing the signed certificate
709+
"""
710+
csr_path = self.certificate_paths[name]
711+
cert_path = self.certificate_signed_paths[name]
712+
cert_pem = self.sign_csr(self.load_certificate(csr_path))
713+
self.write_certificate(cert_path, cert_pem)
714+
return cert_path

0 commit comments

Comments
 (0)