From 1d91fba26cfaedc5ad8f66eb123c5dce8d39e6a8 Mon Sep 17 00:00:00 2001 From: Zolfa Date: Tue, 31 Mar 2020 17:24:36 +0200 Subject: [PATCH 1/4] add default extensions as defined by RFC 5280 x509 v3 extensions keyUsage and extendedKeyUsage should be set always according to RFC 5280 for TLS communications. Last OpenVPN version use these extensions to enforce server and client role, without this settings users must disable server verification. Here also the nsCertType extension is set, even if deprecated by recent versions of OpenVPN, for back-compatibility with older version of OpenVPN server and client. --- ca_manager/models/ssl.py | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/ca_manager/models/ssl.py b/ca_manager/models/ssl.py index e4d93d8..6abf1f1 100644 --- a/ca_manager/models/ssl.py +++ b/ca_manager/models/ssl.py @@ -16,6 +16,12 @@ import json class HostSSLRequest(SignRequest): + x509_extensions = { + 'nsCertType': 'server', + 'keyUsage': 'digitalSignature,keyEncipherment', + 'extendedKeyUsage': 'serverAuth' + } + def __init__(self, req_id, host_name, key_data): super().__init__(req_id) @@ -38,6 +44,12 @@ class HostSSLRequest(SignRequest): class UserSSLRequest(SignRequest): + x509_extensions = { + 'nsCertType': 'client', + 'keyUsage': 'digitalSignature', + 'extendedKeyUsage': 'clientAuth' + } + def __init__(self, req_id, user_name, key_data): super().__init__(req_id) @@ -161,16 +173,26 @@ class SSLAuthority(Authority): with open(pub_key_path, 'w') as stream: stream.write(request.key_data) - subprocess.check_output(['openssl', - 'x509', - '-req', - '-days', self.ca_validity, - '-in', pub_key_path, - '-CA', '%s.pub' % self.path, - '-CAkey', self.path, - '-CAcreateserial', - '-out', cert_path, - '-%s' % self.key_algorithm]) + + cmd = ['openssl', + 'x509', + '-req', + '-days', self.ca_validity, + '-in', pub_key_path, + '-CA', '%s.pub' % self.path, + '-CAkey', self.path, + '-CAcreateserial', + '-out', cert_path, + '-%s' % self.key_algorithm] + + if isinstance(request, (UserSSLRequest, HostSSLRequest)): + cmd += ['-extfile', '-'] + ext_string = '\n'.join( + "{} = {}".format(k, v) for k, v in request.x509_extensions.items() + ) + subprocess.check_output(cmd, input=ext_string.encode()) + else: + subprocess.check_output(cmd) if not self.isRoot: with open(cert_path, 'a') as cert_file: -- 2.30.2 From 23d58b19340df8023322c14eac7e14c1dce1f6fd Mon Sep 17 00:00:00 2001 From: Zolfa Date: Sun, 12 Apr 2020 17:17:13 +0200 Subject: [PATCH 2/4] fix root_ca creation and move to ED25519 keys --- ca_manager/models/ssl.py | 107 +++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/ca_manager/models/ssl.py b/ca_manager/models/ssl.py index 6abf1f1..5083836 100644 --- a/ca_manager/models/ssl.py +++ b/ca_manager/models/ssl.py @@ -19,7 +19,7 @@ class HostSSLRequest(SignRequest): x509_extensions = { 'nsCertType': 'server', 'keyUsage': 'digitalSignature,keyEncipherment', - 'extendedKeyUsage': 'serverAuth' + 'extendedKeyUsage': 'serverAuth', } def __init__(self, req_id, host_name, key_data): @@ -47,7 +47,7 @@ class UserSSLRequest(SignRequest): x509_extensions = { 'nsCertType': 'client', 'keyUsage': 'digitalSignature', - 'extendedKeyUsage': 'clientAuth' + 'extendedKeyUsage': 'clientAuth', } def __init__(self, req_id, user_name, key_data): @@ -109,6 +109,9 @@ class SSLAuthority(Authority): cert_validity = '365' def generate(self): + """ + Generate a Root or non Root Certification Authority + """ if os.path.exists(self.path): raise ValueError('A CA with the same id and type already exists') confirm = input('Is a root CA? [y/N]> ') @@ -117,39 +120,51 @@ class SSLAuthority(Authority): else: self.isRoot = False - subprocess.check_output(['openssl', - 'genrsa', - '-%s' % self.ca_key_algorithm, - '-out', '%s' % (self.path), - self.key_length]) + cmd = [ + 'openssl', + 'genpkey', + '-algorithm', 'ED25519', + '-out', self.path, + ] + + subprocess.check_output(cmd) + + cmd = [ + 'openssl', + 'req', + '-new', + '-key', "{}.key".format(self.path), + ] + if self.isRoot: - subprocess.check_output(['openssl', - 'req', - '-extensions', 'v3_root_ca', - '-config', os.path.join(os.path.dirname(os.path.abspath(getsourcefile(lambda:0))), '../openssl-config/openssl.cnf'), - '-new', - '-x509', - '-days', self.root_ca_validity, - '-key', self.path, - # '-extensions', 'v3_ca' - '-out', '%s.pub' % self.path, - # '-config', "%s.conf"%self.path - ]) + x509_ext = { + 'subjectKeyIdentifier': 'hash', + 'authorityKeyIdentifier': 'keyid:always, issuer', + 'basicConstraints': 'critical, CA:true, pathlen:1', + 'keyUsage': 'cRLSign, keyCertSign', + 'subjectAltName': 'email:copy', + 'issuerAltName': 'issuer:copy', + } + ext_string = '\n'.join( + "{} = {}".format(k, v) for k, v in x509_ext.items() + ) + cmd += [ + '-x509', + '-days', self.root_ca_validity, + '-out', "{}.crt".format(self.path), + '-extfile', '-', + ] + subprocess.check_ourput(cmd, input=ext_string.encode()) else: - subprocess.check_output(['openssl', - 'req', - '-new', - #'-x509', - # '-days', self.ca_validity, - '-key', self.path, - # '-extensions', 'v3_ca' - '-out', '%s.csr' % self.path, - # '-config', "%s.conf"%self.path - ]) + cmd += [ + '-out', "{}.csr".format(self.path), + ] + subprocess.check_output(cmd) + result_dict = {} result_dict['keyType'] = 'ssl_ca' result_dict['caName'] = self.ca_id - with open("%s.csr" % self.path, 'r') as f: + with open("{}.csr".format(self.path), 'r') as f: result_dict['keyData'] = "".join(f.readlines()) request = {'type': 'sign_request', 'request': result_dict} @@ -167,26 +182,29 @@ class SSLAuthority(Authority): if not os.path.exists('%s.pub' % self.path) and not self.isRoot: raise ValueError("The CA certificate '%s.pub' doesn't exists yet" % self.path) - pub_key_path = request.destination + csr_path = request.destination cert_path = request.cert_destination with open(pub_key_path, 'w') as stream: stream.write(request.key_data) - - cmd = ['openssl', - 'x509', - '-req', - '-days', self.ca_validity, - '-in', pub_key_path, - '-CA', '%s.pub' % self.path, - '-CAkey', self.path, - '-CAcreateserial', - '-out', cert_path, - '-%s' % self.key_algorithm] + cmd = [ + 'openssl', + 'x509', + '-req', + '-days', self.ca_validity, + '-in', csr_path, + '-CA', "{}.crt".format(self.path), + '-CAkey', "{}.key".format(self.path), + '-CAcreateserial', + '-out', cert_path, + '-%s' % self.key_algorithm, + ] if isinstance(request, (UserSSLRequest, HostSSLRequest)): - cmd += ['-extfile', '-'] + cmd += [ + '-extfile', '-', + ] ext_string = '\n'.join( "{} = {}".format(k, v) for k, v in request.x509_extensions.items() ) @@ -194,8 +212,9 @@ class SSLAuthority(Authority): else: subprocess.check_output(cmd) + # If it's not a RootCA append the full chain to th output cert if not self.isRoot: with open(cert_path, 'a') as cert_file: - with open('%s.pub' % self.path) as ca_cert_file: + with open("{}.crt".format(self.path), 'r') as ca_cert_file: cert_file.writelines(ca_cert_file.readlines()) return self.ca_validity -- 2.30.2 From cd537f0cb96d75f98c38e514947142f322d73f1f Mon Sep 17 00:00:00 2001 From: Zolfa Date: Sun, 12 Apr 2020 19:52:17 +0200 Subject: [PATCH 3/4] set correct v3 extensions for cas --- ca_manager/models/ssl.py | 50 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/ca_manager/models/ssl.py b/ca_manager/models/ssl.py index 5083836..452dfa6 100644 --- a/ca_manager/models/ssl.py +++ b/ca_manager/models/ssl.py @@ -4,7 +4,6 @@ from playhouse.gfk import * import os -from inspect import getsourcefile import subprocess from .authority import Authority @@ -17,7 +16,6 @@ import json class HostSSLRequest(SignRequest): x509_extensions = { - 'nsCertType': 'server', 'keyUsage': 'digitalSignature,keyEncipherment', 'extendedKeyUsage': 'serverAuth', } @@ -45,7 +43,6 @@ class HostSSLRequest(SignRequest): class UserSSLRequest(SignRequest): x509_extensions = { - 'nsCertType': 'client', 'keyUsage': 'digitalSignature', 'extendedKeyUsage': 'clientAuth', } @@ -72,6 +69,15 @@ class UserSSLRequest(SignRequest): class CASSLRequest(SignRequest): + x509_extensions = { + 'subjectKeyIdentifier': 'hash', + 'authorityKeyIdentifier': 'keyid:always, issuer', + 'basicConstraints': 'critical, CA:true', + 'keyUsage': 'cRLSign, keyCertSign', + 'subjectAltName': 'email:copy', + 'issuerAltName': 'issuer:copy', + } + def __init__(self, req_id, ca_name, key_data): super().__init__(req_id) @@ -123,8 +129,9 @@ class SSLAuthority(Authority): cmd = [ 'openssl', 'genpkey', + '-aes256', '-algorithm', 'ED25519', - '-out', self.path, + '-out', "{}.key".format(self.path), ] subprocess.check_output(cmd) @@ -145,16 +152,18 @@ class SSLAuthority(Authority): 'subjectAltName': 'email:copy', 'issuerAltName': 'issuer:copy', } - ext_string = '\n'.join( - "{} = {}".format(k, v) for k, v in x509_ext.items() - ) + cmd += [ '-x509', '-days', self.root_ca_validity, '-out', "{}.crt".format(self.path), - '-extfile', '-', - ] - subprocess.check_ourput(cmd, input=ext_string.encode()) + ] + + for k, v in x509_ext.items(): + cmd += ['-addext', "{}={}".format(k, v)] + + subprocess.check_output(cmd) + else: cmd += [ '-out', "{}.csr".format(self.path), @@ -185,7 +194,7 @@ class SSLAuthority(Authority): csr_path = request.destination cert_path = request.cert_destination - with open(pub_key_path, 'w') as stream: + with open(csr_path, 'w') as stream: stream.write(request.key_data) cmd = [ @@ -198,21 +207,16 @@ class SSLAuthority(Authority): '-CAkey', "{}.key".format(self.path), '-CAcreateserial', '-out', cert_path, - '-%s' % self.key_algorithm, + '-extfile', '-', ] - if isinstance(request, (UserSSLRequest, HostSSLRequest)): - cmd += [ - '-extfile', '-', - ] - ext_string = '\n'.join( - "{} = {}".format(k, v) for k, v in request.x509_extensions.items() - ) - subprocess.check_output(cmd, input=ext_string.encode()) - else: - subprocess.check_output(cmd) + ext_string = '\n'.join( + "{} = {}".format(k, v) for k, v in request.x509_extensions.items() + ) + + subprocess.check_output(cmd, input=ext_string.encode('utf-8')) - # If it's not a RootCA append the full chain to th output cert + # If it's not a RootCA append the full chain to the output cert if not self.isRoot: with open(cert_path, 'a') as cert_file: with open("{}.crt".format(self.path), 'r') as ca_cert_file: -- 2.30.2 From 796bcfcf92b73567013be996e10c52d8b8dc9538 Mon Sep 17 00:00:00 2001 From: Zolfa Date: Mon, 13 Apr 2020 20:11:31 +0200 Subject: [PATCH 4/4] improved handling of x509 exts Each root CA, intermediate CA, host Certificate and user Certificate will be set up with proper x509_v3 extensions. Default Key format is ED25519, options are available for RSA and EC. --- ca_manager/models/ssl.py | 129 +++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/ca_manager/models/ssl.py b/ca_manager/models/ssl.py index 452dfa6..bd6095c 100644 --- a/ca_manager/models/ssl.py +++ b/ca_manager/models/ssl.py @@ -5,6 +5,7 @@ from playhouse.gfk import * import os import subprocess +from tempfile import NamedTemporaryFile from .authority import Authority from .certificate import Certificate @@ -15,8 +16,11 @@ import json class HostSSLRequest(SignRequest): - x509_extensions = { - 'keyUsage': 'digitalSignature,keyEncipherment', + v3_exts = { + 'subjectKeyIdentifier': 'hash', + 'authorityKeyIdentifier': 'keyid:always, issuer', + 'basicConstraints': 'critical, CA:FALSE', + 'keyUsage': 'critical, digitalSignature, keyEncipherment', 'extendedKeyUsage': 'serverAuth', } @@ -42,8 +46,11 @@ class HostSSLRequest(SignRequest): class UserSSLRequest(SignRequest): - x509_extensions = { - 'keyUsage': 'digitalSignature', + v3_exts = { + 'subjectKeyIdentifier': 'hash', + 'authorityKeyIdentifier': 'keyid:always, issuer', + 'basicConstraints': 'critical, CA:FALSE', + 'keyUsage': 'critical, digitalSignature', 'extendedKeyUsage': 'clientAuth', } @@ -69,10 +76,10 @@ class UserSSLRequest(SignRequest): class CASSLRequest(SignRequest): - x509_extensions = { + v3_exts = { 'subjectKeyIdentifier': 'hash', 'authorityKeyIdentifier': 'keyid:always, issuer', - 'basicConstraints': 'critical, CA:true', + 'basicConstraints': 'critical, CA:true, pathlen:0', 'keyUsage': 'cRLSign, keyCertSign', 'subjectAltName': 'email:copy', 'issuerAltName': 'issuer:copy', @@ -106,10 +113,24 @@ class SSLAuthority(Authority): CASSLRequest, ] - ca_key_algorithm = 'des3' - key_length = '4096' + key_encryption = 'aes256' + + key_format = 'ED25519' + key_format_extra = {} + + #key_format = 'RSA' + #key_format_extra = { + # 'rsa_keygen_bits': 4096, + # 'rsa_keygen_primes': 2, + # 'rsa_keygen_pubexp': 65537, + #} + + #key_format = 'EC' + #key_format_extra = { + # 'ec_paramgen_curve': 'P-256', + # 'ec_param_enc': 'named_curve', + #} - key_algorithm = 'sha256' root_ca_validity = '3650' ca_validity = '1825' cert_validity = '365' @@ -126,25 +147,31 @@ class SSLAuthority(Authority): else: self.isRoot = False + # Create Private Key cmd = [ 'openssl', 'genpkey', - '-aes256', - '-algorithm', 'ED25519', + '-{}'.format(self.key_encryption), '-out', "{}.key".format(self.path), + '-algorithm', self.key_format, ] - + for k, v in self.key_format_extra.items(): + cmd += ['-pkeyopt', '{}:{}'.format(k, v)] subprocess.check_output(cmd) + # Create Certificate Request cmd = [ 'openssl', 'req', '-new', '-key', "{}.key".format(self.path), + '-out', "{}.csr".format(self.path), ] + subprocess.check_output(cmd) if self.isRoot: - x509_ext = { + # If CA is Root, generate self signed certificate + v3_exts = { 'subjectKeyIdentifier': 'hash', 'authorityKeyIdentifier': 'keyid:always, issuer', 'basicConstraints': 'critical, CA:true, pathlen:1', @@ -152,44 +179,45 @@ class SSLAuthority(Authority): 'subjectAltName': 'email:copy', 'issuerAltName': 'issuer:copy', } - - cmd += [ - '-x509', - '-days', self.root_ca_validity, - '-out', "{}.crt".format(self.path), - ] - - for k, v in x509_ext.items(): - cmd += ['-addext', "{}={}".format(k, v)] - - subprocess.check_output(cmd) + with NamedTemporaryFile(mode='w') as extfile: + extfile.writelines( + ["{} = {}\n".format(k, v) + for k, v in v3_exts.items()]) + extfile.flush() + subprocess.check_output([ + 'openssl', + 'x509', + '-req', + '-days', self.root_ca_validity, + '-in', "{}.csr".format(self.path), + '-signkey', "{}.key".format(self.path), + '-out', "{}.crt".format(self.path), + '-extfile', extfile.name, + ]) else: - cmd += [ - '-out', "{}.csr".format(self.path), - ] - subprocess.check_output(cmd) - + # If CA is not Root, format a JSON signing request result_dict = {} result_dict['keyType'] = 'ssl_ca' result_dict['caName'] = self.ca_id with open("{}.csr".format(self.path), 'r') as f: result_dict['keyData'] = "".join(f.readlines()) - request = {'type': 'sign_request', 'request': result_dict} print('Please sign the following request:') print(json.dumps(request)) + # Init CA serial with open(self.path + '.serial', 'w') as stream: - stream.write(str(0)) + stream.write('01\n') def generate_certificate(self, request): """ Sign a *SSLRequest with this certification authority """ - if not os.path.exists('%s.pub' % self.path) and not self.isRoot: - raise ValueError("The CA certificate '%s.pub' doesn't exists yet" % self.path) + if not os.path.exists('%s.crt' % self.path) and not self.isRoot: + raise ValueError( + "The CA certificate '%s.crt' doesn't exists yet" % self.path) csr_path = request.destination cert_path = request.cert_destination @@ -197,24 +225,23 @@ class SSLAuthority(Authority): with open(csr_path, 'w') as stream: stream.write(request.key_data) - cmd = [ - 'openssl', - 'x509', - '-req', - '-days', self.ca_validity, - '-in', csr_path, - '-CA', "{}.crt".format(self.path), - '-CAkey', "{}.key".format(self.path), - '-CAcreateserial', - '-out', cert_path, - '-extfile', '-', - ] - - ext_string = '\n'.join( - "{} = {}".format(k, v) for k, v in request.x509_extensions.items() - ) - - subprocess.check_output(cmd, input=ext_string.encode('utf-8')) + with NamedTemporaryFile(mode='w') as extfile: + extfile.writelines( + ["{} = {}\n".format(k, v) + for k, v in request.v3_exts.items()]) + extfile.flush() + subprocess.check_output([ + 'openssl', + 'x509', + '-req', + '-days', self.ca_validity, + '-in', csr_path, + '-CA', "{}.crt".format(self.path), + '-CAkey', "{}.key".format(self.path), + '-CAserial', "{}.serial".format(self.path), + '-out', cert_path, + '-extfile', extfile.name, + ]) # If it's not a RootCA append the full chain to the output cert if not self.isRoot: -- 2.30.2