11import os
22import ssl
33import 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.
78from cryptography import x509
8- from cryptography .x509 .oid import NameOID
9+ from cryptography .x509 .oid import NameOID , ExtendedKeyUsageOID
910from cryptography .hazmat .primitives import hashes , serialization
1011from cryptography .hazmat .primitives .asymmetric import rsa
12+ from cryptography .hazmat .primitives .serialization import BestAvailableEncryption
13+
1114
1215from 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