commit 72e8b513a1b054ead2c0cb2a8dcee01a71904865 Author: Kevin Guerineau Date: Tue Jan 31 22:04:12 2023 +0100 [ADD] First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8495cfb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +samba-pki-tools.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cfee48 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +apt install python3-jinja2 diff --git a/samba-pki-tools.ini.sample b/samba-pki-tools.ini.sample new file mode 100644 index 0000000..62421dc --- /dev/null +++ b/samba-pki-tools.ini.sample @@ -0,0 +1,19 @@ +[general] + +pki_dir = /opt/pki + +[openssl_config] +root_name = KG TIS +country = FR +state = Pays de la Loire +city = Saint Sebastien Sur Loire +organization_name = KG Tranquil IT +organization_ou = KG Tranquil IT CA +organization_cn = KG Tranquil IT Root CA + +default_cert_duration = 730 + +crl_uri = URI:http://crl.kg.tranquil.it/crl/root_ca.crl + +[samba_ad] +dc_list = diff --git a/templates/openssl_root_ca.tmpl b/templates/openssl_root_ca.tmpl new file mode 100644 index 0000000..5d96489 --- /dev/null +++ b/templates/openssl_root_ca.tmpl @@ -0,0 +1,53 @@ +[ ca ] +default_ca = {{ organization_ou }} + +[ CA_default ] +dir = {{ pki_dir }} +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +private_key = $dir/private/root_ca.key +RANDFILE = $dir/private/.rand + +default_md = sha512 + +name_opt = ca_default +cert_opt = ca_default +default_days = {{ default_cert_duration }} +preserve = no +policy = policy_strict + +[ policy_strict ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +defaults_bits = 4096 +distinguished_name = req_distinguished_name +string_mask = utf8only +prompt = no + +default_md = sha512 + +[ req_distinguished_name ] +C = {{ country }} +ST = {{ state }} +L = {{ city }} +O = {{ organization_name }} +OU = {{ organization_ou }} +CN = {{ organization_cn }} + +[ v3_ca ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +crlDistributionPoints = {{ crl_uri }} + diff --git a/templates/openssl_server_cert.tmpl b/templates/openssl_server_cert.tmpl new file mode 100644 index 0000000..b529787 --- /dev/null +++ b/templates/openssl_server_cert.tmpl @@ -0,0 +1,131 @@ +set_dns = {{ dc_name }} +set_dc_guid = {{ dc_guid }} + +set_crp_default = {{ crl_uri }} +CRLDISTPT = $set_crp_default + +oid_section = new_oids + +[ new_oids ] +msADGUID=1.3.6.1.4.1.311.25.1 + +[ ca ] +# `man ca` +default_ca = CA_default + +[ CA_default ] +# Directory and file locations. +dir = {{ pki_dir }} +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +private = $dir/private + +# The root key and root certificate. +private_key = $dir/private/root_ca.key +certificate = $dir/certs/root_ca.crt + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha512 + +name_opt = ca_default +cert_opt = ca_default +default_days = {{ default_crl_duration }} +preserve = no +policy = policy_loose + +default_crl_days = 90 + +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name + +[ policy_loose ] +# Allow the intermediate CA to sign a more diverse range of certificates. +# See the POLICY FORMAT section of the `ca` man page. +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ server_cert ] +# Extensions for server certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = server +nsComment = "OpenSSL Generated Server Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = {{ country }} + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = {{ state }} + +localityName = Locality Name (eg, city) +localityName_default = {{ city }} + +organizationName = Organization Name +organizationName_default = {{ organization_name }} + +organizationalUnitName = Organizational Unit Name +organizationalUnitName_default = {{ organization_unit }} + +commonName = Common Name (eg, Your Name or server name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +########################################## Domain Controller Certificate ##################################### +[ usr_cert_mskdc ] + +# These extensions are added when 'ca' signs a request for a domain controller certificate. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE +crlDistributionPoints=URI:$CRLDISTPT + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +nsCertType = server + +# This is typical in keyUsage for a client certificate. +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "Domain Controller Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. + +subjectAltName=@dc_subjalt + +# Copy subject details +issuerAltName=issuer:copy + +nsCaRevocationUrl = $CRLDISTPT + +#Extended Key requirements for our domain controller certs +# serverAuth - says cert can be used to identify an ssl/tls server +# pkInitKDC - says cert can be used to identify a Kerberos Domain Controller. +extendedKeyUsage = clientAuth,serverAuth,pkInitKDC + +[dc_subjalt] +DNS=$set_dns +otherName=msADGUID;FORMAT:HEX,OCTETSTRING:$set_dc_guid diff --git a/templates/openssl_user_cert.tmpl b/templates/openssl_user_cert.tmpl new file mode 100644 index 0000000..c984fc6 --- /dev/null +++ b/templates/openssl_user_cert.tmpl @@ -0,0 +1,121 @@ +set_crp_default = {{ crl_uri }} +CRLDISTPT = $set_crp_default + +[ ca ] +# `man ca` +default_ca = CA_default + +[ CA_default ] +# Directory and file locations. +dir = {{ pki_dir }} +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +private = $dir/private + +# The root key and root certificate. +private_key = $dir/private/root_ca.key +certificate = $dir/certs/root_ca.crt + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha512 + +name_opt = ca_default +cert_opt = ca_default +default_days = {{ default_crl_duration }} +preserve = no +policy = policy_loose + +default_crl_days = 90 + +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name + +[ policy_loose ] +# Allow the intermediate CA to sign a more diverse range of certificates. +# See the POLICY FORMAT section of the `ca` man page. +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ server_cert ] +# Extensions for server certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = server +nsComment = "OpenSSL Generated Server Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = {{ country }} + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = {{ state }} + +localityName = Locality Name (eg, city) +localityName_default = {{ city }} + +organizationName = Organization Name +organizationName_default = {{ organization_name }} + +organizationalUnitName = Organizational Unit Name +organizationalUnitName_default = {{ organization_unit }} + +commonName = Common Name (eg, Your Name or server name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +########################################### User Certificates ################################################ +[ usr_cert_scarduser ] + +# These extensions are added when 'ca' signs a request for a certificate that will be used to login from a smart card + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE +crlDistributionPoints=URI:$CRLDISTPT + +# For normal client use this is typical +nsCertType = client, email + +# This is typical in keyUsage for a client certificate. +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "Smart Card Login Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. + +#subjectAltName=email:copy,otherName:msUPN;UTF8:kguerineau@ad.tranqui.it +subjectAltName = @sanuser + +# Copy subject details +issuerAltName=issuer:copy + +nsCaRevocationUrl = $CRLDISTPT +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +#Extended Key requirements for client certs +extendedKeyUsage = clientAuth,msSmartcardLogin + diff --git a/tis-pki.py b/tis-pki.py new file mode 100644 index 0000000..6beef41 --- /dev/null +++ b/tis-pki.py @@ -0,0 +1,230 @@ +#!/usr/bin/python3 + +import subprocess +import jinja2 +import os +import configparser +import sys + +config = configparser.ConfigParser() +config.read('samba-pki-tools.ini') + +# # write config file +# jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_3cx)) +# template = jinja_env.get_template("3CXVoipPhone.j2") +# template_variables = { +# "name": "TIS (%s)" % get_current_user(), } + +class TisPKI: + + def pki_dir(): + return config.get('general','pki_dir') + + def root_ca_certfile(): + return os.path.join(TisPKI.pki_dir(),'certs','root_ca.crt') + + def keyout_path(): + return os.path.join(TisPKI.pki_dir(),'private') + + def csr_path(): + return os.path.join(TisPKI.pki_dir(),'csr') + + def cert_path(): + return os.path.join(TisPKI.pki_dir(),'certs') + + def p12_path(): + return os.path.join(TisPKI.pki_dir(),'p12') + +def check_directories(): + print('Check directories') + + directories_list = ['certs','config','crl','newcerts','private','csr','crl','p12'] + + if not os.path.isdir(TisPKI.pki_dir()): + print(f'Create { TisPKI.pki_dir() } directory') + os.makedirs(TisPKI.pki_dir()) + + for directory in directories_list: + directory_path = os.path.join(TisPKI.pki_dir(),directory) + if not os.path.isdir(directory_path): + print(f'Create { directory_path } directory') + os.makedirs(directory_path) + + if not os.path.isfile(os.path.join(TisPKI.pki_dir(),'index.txt')): + with open(os.path.join(TisPKI.pki_dir(),'index.txt'),'w') as file: + pass + + +def create_openssl_config(): + print('Check Root CA OpenSSL Config') + + root_ca_config = os.path.join(TisPKI.pki_dir(),'config','openssl_root_ca.ini') + root_ca_keyfile = os.path.join(TisPKI.pki_dir(),'private','root_ca.key') + + if not os.path.isfile(root_ca_config): + print('Root CA OpenSSL configfile not exist. Creating...') + + template_dir = os.path.join('templates') + jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) + root_ca_tmpl = jinja_env.get_template('openssl_root_ca.tmpl') + root_ca_tmpl_var = { + 'organization_ou': config.get('openssl_config','organization_name') + 'CA', + 'default_cert_duration': config.get('openssl_config','default_cert_duration'), + 'pki_dir': TisPKI.pki_dir(), + 'country': config.get('openssl_config','country'), + 'state': config.get('openssl_config','state'), + 'city': config.get('openssl_config','city'), + 'organization_name': config.get('openssl_config','organization_name'), + 'organization_ou': config.get('openssl_config','organization_ou'), + 'organization_cn': config.get('openssl_config','organization_cn'), + 'crl_uri': config.get('openssl_config','crl_uri') + } + + config_string = root_ca_tmpl.render(root_ca_tmpl_var) + with open(root_ca_config,'wt') as file: + file.write(config_string) + + if os.path.isfile(root_ca_config): + print('Root CA OpenSSL config file is correctly generated !') + else: + print('Root CA OpenSSL config already exist. Skip.') + + # Generate privkey and cert for Root CA + if not os.path.isfile(root_ca_keyfile) and not os.path.isfile(TisPKI.root_ca_certfile()): + print('Generate CA private key') + gen_root_ca = subprocess.run(f'/usr/bin/openssl req -x509 -new -sha512 -config {root_ca_config} -days 3650 -extensions v3_ca -keyout {root_ca_keyfile} -out {TisPKI.root_ca_certfile()} -passout pass:calimero', shell=True, check=True, executable='/bin/bash') + + if gen_root_ca.returncode == 0: + print(subprocess.run(f'openssl x509 -in {TisPKI.root_ca_certfile()} -text -noout', shell=True, check=True, executable='/bin/bash')) + else: + print('Error on generating Root CA private key') + sys.exit(1) + else: + print('Root CA private key and certificate already exist. Skip.') + + +def generate_dc_certificate(): + dc_list = config.get('samba_ad','dc_list') + + for dc in dc_list.split(','): + dc_certfile = os.path.join(TisPKI.pki_dir(),'certs',f'{dc}.crt') + dc_keyfile = os.path.join(TisPKI.pki_dir(),'private',f'{dc}.key') + dc_csrfile = os.path.join(TisPKI.pki_dir(),'csr',f'{dc}.csr') + dc_openssl_configfile = os.path.join(TisPKI.pki_dir(),'config',f'openssl_{dc}.ini') + crl_file = os.path.join(TisPKI.pki_dir(),'crl','root_ca.crl') + if not os.path.isfile(dc_certfile) and not os.path.isfile(dc_keyfile): + print(f'Generate certificate for {dc}') + #dc_guid = subprocess.run(f'get_guid.sh {dc}',shell=True) + dc_guid = '51a10f19f84f8d4abda4dee35fe096c6' + print(dc_guid) + template_dir = ('templates') + jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) + dc_tmpl = jinja_env.get_template('openssl_server_cert.tmpl') + dc_tmpl_var = { + 'dc_name': f"{dc}", + 'dc_guid': dc_guid, + 'crl_uri': config.get('openssl_config','crl_uri'), + 'pki_dir': TisPKI.pki_dir(), + 'default_cert_duration': config.get('openssl_config','default_cert_duration'), + 'country': config.get('openssl_config','country'), + 'state': config.get('openssl_config','state'), + 'city': config.get('openssl_config','city'), + 'organization_name': config.get('openssl_config','organization_name'), + 'organization_ou': config.get('openssl_config','organization_ou') + } + + config_string = dc_tmpl.render(dc_tmpl_var) + with open(dc_openssl_configfile,'wt') as file: + file.write(config_string) + + if os.path.isfile(dc_openssl_configfile): + print(f'{dc} OpenSSL configfile is correctly generated !') + + print(f'Generate private key and CSR for {dc}') + print(subprocess.run(f"openssl req -new -addext 'subjectAltName = email:copy' -newkey rsa:4096 -keyout {dc_keyfile} -out {dc_csrfile} -config {dc_openssl_configfile} -passout pass:calimero" , shell=True, check=True, executable='/bin/bash')) + + if os.path.isfile(dc_csrfile): + print(f'Sign certificate for {dc}') + print(subprocess.run(f'openssl ca -config {dc_openssl_configfile} -extensions usr_cert_mskdc -days 3650 -notext -md sha512 -create_serial -in {dc_csrfile} -out {dc_certfile}', shell=True, check=True, executable='/bin/bash')) + + if os.path.isfile(dc_certfile): + print('Concatenation of DC and Root cert') + subprocess.run(f'cat {dc_certfile} {TisPKI.root_ca_certfile()} > {dc_certfile}_full',shell=True) + + else: + print(f'{dc} private key and certificate already exist ! Revoke certificate before regenerate') + + print('Generate CRL') + subprocess.run(f'openssl ca -config {dc_openssl_configfile} -gencrl -out {crl_file}',shell=True) + + +def generate_user_certificate(): + + openssl_user_file = os.path.join(TisPKI.pki_dir(),'config','openssl_user.ini') + + template_dir = ('templates') + jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) + user_tmpl = jinja_env.get_template('openssl_user_cert.tmpl') + user_tmpl_var = { + 'crl_uri': config.get('openssl_config','crl_uri'), + 'pki_dir': TisPKI.pki_dir(), + 'default_cert_duration': config.get('openssl_config','default_cert_duration'), + 'country': config.get('openssl_config','country'), + 'state': config.get('openssl_config','state'), + 'city': config.get('openssl_config','city'), + 'organization_name': config.get('openssl_config','organization_name'), + 'organization_ou': config.get('openssl_config','organization_ou') + } + + config_string = user_tmpl.render(user_tmpl_var) + with open(openssl_user_file,'wt') as file: + file.write(config_string) + + if os.path.isfile(openssl_user_file): + print(f'User OpenSSL configfile is correctly generated !') + + + print('Enter username') + + username = input() + upn = f'{username}@kg.tranquil.it' + + print(f'Generate private key for {upn}') + print(subprocess.run(f"openssl req -new -newkey rsa:4096 -keyout {TisPKI.keyout_path()}/{username}.key -out {TisPKI.csr_path()}/{username}.csr -config <(cat {openssl_user_file} <(cat <<-EOF\n[ sanuser ]\notherName=msUPN;UTF8:{upn}\nemail=copy\nEOF\n)\n)",shell=True,check=True, executable='/bin/bash')) + + print(f'Sign certificate') + print(subprocess.run(f'openssl ca -extensions usr_cert_scarduser -days 730 -notext -md sha512 -create_serial -in {TisPKI.csr_path()}/{username}.csr -out {TisPKI.cert_path()}/{username}.crt -config <(cat {openssl_user_file} <(cat <<-EOF\n[ sanuser ]\notherName=msUPN;UTF8:{upn}\nemail=copy\nEOF\n)\n)',shell=True,check=True, executable='/bin/bash')) + + + print('Remove password in rsa key') + print(subprocess.run(f'openssl rsa -in {TisPKI.keyout_path()}/{username}.key -out {TisPKI.keyout_path()}/{username}-nopasswd.key',shell=True,check=True, executable='/bin/bash')) + + print('Create p12') + print(subprocess.run(f'openssl pkcs12 -export -inkey {TisPKI.keyout_path()}/{username}-nopasswd.key -in {TisPKI.cert_path()}/{username}.crt -out {TisPKI.p12_path()}/{username}.p12', shell=True,check=True, executable='/bin/bash')) + + +def main(): + + if config.get('general','pki_dir'): + check_directories() + else: + print('No pki_dir set in samba-pki-tools.ini') + sys.exit(1) + + if config.get('openssl_config','root_name'): + create_openssl_config() + else: + print('No root_name set in samba-pki-tools.ini') + sys.exit(1) + + if config.get('samba_ad','dc_list'): + generate_dc_certificate() + else: + print('No dc_list set in samba-pki-tools.ini') + sys.exit(1) + + generate_user_certificate() + + +if __name__ == '__main__': + main()