You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

431 lines
18 KiB

#!/usr/bin/python
import sys
import os
import json
import logging
import argparse
import subprocess
import shutil
def check_directory(logger, directory, confirm=False):
"""create directory if not exists"""
if False == os.path.exists(directory):
if True == confirm:
while True:
user_input = input('do you want to create %s [Y/n] ? ' % directory)
if user_input in ['Y', 'y', '']:
break;
elif user_input in ['N', 'n']:
logger.error('aborted')
sys.exit(2)
os.mkdir(directory)
if False == os.path.exists(directory):
logger.error('%s not found' % directory);
sys.exit(1)
if False == os.path.isdir(directory):
logger.error('%s is not a directory' % directory);
sys.exit(1)
if False == os.access(directory, os.W_OK):
logger.error('%s not writable' % directory);
sys.exit(1)
def create_config_file(logger, config):
"""write the letsencrypt configuration file"""
if True == os.path.exists(config['filenames']['cli']) and ( True == os.path.isdir(config['filenames']['cli']) or False == os.access(config['filenames']['cli'], os.W_OK) ):
logger.error('%s not writeable' % config['filenames']['cli'])
sys.exit(1)
try:
cli_file = open(config['filenames']['cli'], 'w')
except IOError:
logger.error('could not open %s' % config['filenames']['cli'])
cli_file.write('config-dir = %s\n'
'work-dir = %s\n'
'logs-dir = /var/log/letsencrypt/%s/\n'
'email = %s\n'
'#domains = test.confais.org\n'
'#csr=/root/test.confais.org.csr.der \n'
'text = True\n'
'agree-tos = True\n'
'debug = True\n'
'verbose = True\n'
'authenticator = %s\n'
'%s'
'server = %s\n'
'no-verify-ssl = True\n' % (config['directory'], config['directory'], config['chain'], config['letsencrypt']['email'], config['letsencrypt']['type'], (('webroot-path = %s\n' % config['letsencrypt']['webroot-path']) if 'webroot' == config['letsencrypt']['type'] else ''), config['letsencrypt']['server']))
cli_file.close()
def create_next_key(logger, config):
"""create a rsa key"""
if True == os.path.exists(config['filenames']['next']['key']) and ( True == os.path.isdir(config['filenames']['next']['key']) or False == os.access(config['filenames']['next']['key'], os.W_OK) ):
logger.error('%s not writeable' % config['filenames']['next']['key'])
sys.exit(1)
if False == os.path.exists(config['filenames']['next']['key']):
try:
subprocess.check_call(['openssl', 'genrsa', '-out', config['filenames']['next']['key'], '4096'])
except subprocess.CalledProcessError as e :
logger.error('next key creation failed (return %d)' % e.returncode)
sys.exit(1)
os.chown(config['filenames']['next']['key'], 0, 0)
os.chmod(config['filenames']['next']['key'], 0o400)
def create_next_csr(logger, config):
"""create a csr with a dns alt name"""
if True == os.path.exists(config['filenames']['next']['csr']['pem']) and ( True == os.path.isdir(config['filenames']['next']['csr']['pem']) or False == os.access(config['filenames']['next']['csr']['pem'], os.W_OK) ):
logger.error('%s not writeable' % config['filenames']['next']['csr']['pem'])
sys.exit(1)
if True == os.path.exists(config['filenames']['next']['csr']['der']) and ( True == os.path.isdir(config['filenames']['next']['csr']['der']) or False == os.access(config['filenames']['next']['csr']['der'], os.W_OK) ):
logger.error('%s not writeable' % config['filenames']['next']['csr']['der'])
sys.exit(1)
try:
if config['other_domains']:
o = []
for d in config['other_domains'][0].split(','):
o.append('DNS:' + d)
subprocess.check_call(['bash', '-c', 'openssl req -new -key "' + config['filenames']['next']['key'] + '" -out "' + config['filenames']['next']['csr']['pem'] + '" -subj "' + config['cn'] + '" -reqexts cert -config <(cat /etc/ssl/openssl.cnf <(printf "[ cert ]\nsubjectAltName=DNS:' + config['domain'] + ',' + ','.join(o) + '"))'])
else:
subprocess.check_call(['bash', '-c', 'openssl req -new -key "' + config['filenames']['next']['key'] + '" -out "' + config['filenames']['next']['csr']['pem'] + '" -subj "' + config['cn'] + '" -reqexts cert -config <(cat /etc/ssl/openssl.cnf <(printf "[ cert ]\nsubjectAltName=DNS:' + config['domain'] + '"))'])
except subprocess.CalledProcessError as e :
logger.error('next csr creation failed (return %d)' % e.returncode)
sys.exit(1)
try:
subprocess.check_call(['openssl', 'req', '-inform', 'pem', '-outform', 'der', '-in', config['filenames']['next']['csr']['pem'], '-out', config['filenames']['next']['csr']['der']])
except subprocess.CalledProcessError as e :
logger.error('next csr conversion to der failed (return %d)' % e.returncode)
sys.exit(1)
def create_dh(logger, config):
try:
for size in [512, 1024, 2048]:
file = '%s%s' % (config['filenames']['live']['dh'], str(size))
subprocess.check_call(['openssl', 'dhparam', '-dsaparam', '-out', '%s' % (file), '%s' % str(size) ])
os.chown(file, 0, 0)
os.chmod(file, 0o444)
except subprocess.CalledProcessError as e :
logger.error('dh params creation failed (return %d)' % e.returncode)
sys.exit(1)
def move_files(logger, config):
"""replace the old certificate with the fresh one"""
# move certificates
logger.info('check permissions on files')
for file in [ config['filenames']['current']['key'], config['filenames']['current']['csr'], config['filenames']['archive']['key'], config['filenames']['archive']['cert'], config['filenames']['live']['cert'], config['filenames']['live']['chain'], config['filenames']['live']['fullchain'] ]:
if True == os.path.exists(file) and ( True == os.path.isdir(file) or False == os.access(file, os.W_OK) ):
logger.error('%s not writeable' % file)
sys.exit(1)
# backup to archives
logger.info('backup the old certificate')
try:
shutil.copy2(config['filenames']['current']['key'], config['filenames']['archive']['key'])
shutil.copy2(config['filenames']['live']['cert'], config['filenames']['archive']['cert'])
except:
logger.error('cannot copy current key to archives')
# move files
logger.info('move the certificate')
try:
shutil.move(config['filenames']['next']['key'], config['filenames']['current']['key'])
shutil.move(config['filenames']['next']['csr']['pem'], config['filenames']['current']['csr'])
shutil.move('0000_cert.pem', config['filenames']['live']['cert'])
shutil.move('0000_chain.pem', config['filenames']['live']['chain'])
shutil.move('0001_chain.pem', config['filenames']['live']['fullchain'])
except:
logger.error('cannot move some files')
shutil.copy2(config['filenames']['archive']['key'], config['filenames']['current']['key'])
shutil.copy2(config['filenames']['archive']['cert'], config['filenames']['live']['cert'])
logger.info('set permissions')
for file in [config['filenames']['current']['key'], config['filenames']['archive']['key']]:
if True == os.path.exists(file):
os.chown(file, 0, 0)
os.chmod(file, 0o400)
for file in [config['filenames']['current']['csr'], config['filenames']['live']['cert'], config['filenames']['live']['chain'], config['filenames']['live']['fullchain']]:
if True == os.path.exists(file):
os.chown(file, 0, 0)
os.chmod(file, 0o444)
if 'confais' == config['chain']:
try:
subprocess.check_call(['bash', '-c', 'cat "' + config['filenames']['live']['cert'] + '" "' + config['filenames']['live']['chain'] +'" "/usr/share/ca-certificates/trust-source/anchors/confais.org.root.pem" > "' + config['filenames']['live']['fullchain_with_root'] + '"'])
os.chown(config['filenames']['live']['fullchain_with_root'], 0, 0)
os.chmod(config['filenames']['live']['fullchain_with_root'], 0o444)
except subprocess.CalledProcessError as e :
logger.error('cannot create fullchain with root (return %d)' % e.returncode)
if 'letsencrypt' == config['chain']:
try:
subprocess.check_call(['bash', '-c', 'ct-submit ct.googleapis.com/pilot < "' + config['filenames']['live']['chain'] + '" > "' + config['filenames']['live']['sct'] + '"'])
os.chown(config['filenames']['live']['sct'], 0, 0)
os.chmod(config['filenames']['live']['sct'], 0o444)
except subprocess.CalledProcessError as e :
logger.error('cannot create certificate transparency (return %d)' % e.returncode)
logger.info('remove the csr in der format')
if True == os.path.exists(config['filenames']['next']['csr']['der']):
os.remove(config['filenames']['next']['csr']['der'])
def info(logger, config):
"""print some informations about certificate"""
pins = []
for file in [config['filenames']['current']['key'], config['filenames']['next']['key']]:
try:
ret = subprocess.check_output(['bash', '-c', 'openssl rsa -in ' + file + ' -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64 | tr -d \'\n\''])
pins.append(ret)
except subprocess.CalledProcessError as e :
logger.error('cannot (return %d)' % e.returncode)
continue
header = 'add_header Public-Key-Pins \''
for pin in pins:
header += 'pin-sha256="' + str(pin.decode('ascii')) + '"; '
header += 'max-age=2592000\';'
print('========== HPKP header ==========')
print(header)
# tlsa
logger.info('TLSA record')
try:
ret = subprocess.check_output(['ldns-dane', '-c', config['filenames']['live']['cert'], 'create', config['domain'], '443', '3', '1', '2' ])
print('========== TLSA record ==========')
sys.stdout.write(ret.decode('ascii'))
except subprocess.CalledProcessError as e :
logger.error('unable generate TLSA record (return %d)' % e.returncode )
# SHA1
logger.info('SHA1')
try:
ret = subprocess.check_output(['openssl', 'x509', '-in', config['filenames']['live']['cert'], '-sha1', '-noout', '-fingerprint'])
print('========== SHA1 (to allow mail forward) ==========')
sys.stdout.write(ret.decode('ascii'))
except subprocess.CalledProcessError as e :
logger.error('unable generate TLSA record (return %d)' % e.returncode )
print('==========\n If the computer has a webserver available through ipv4, don\'t forget to send a copy of the certificate to the reverse proxy\n==========')
def autosign(logger, config):
"""autosign the next key (for webroot)"""
try:
subprocess.check_call(['bash', '-c', 'openssl x509 -req -days 1 -in "' + config['filenames']['next']['csr']['pem'] + '" -signkey "' + config['filenames']['next']['key'] + '" -out "/tmp/webroot.pem" -sha256 -extensions cert -extfile <(cat /etc/ssl/openssl.cnf <(printf "[ cert ]\nsubjectAltName=DNS:' + config['domain'] + '"))'])
except subprocess.CalledProcessError as e :
logger.error('autosigned certificate creation failed (return %d)' % e.returncode)
sys.exit(1)
def create_nginx_config_file(logger, config):
"""create a temporary nginx configuration file"""
nginx_filename = '/tmp/webroot.conf'
if True == os.path.exists(nginx_filename) and ( True == os.path.isdir(nginx_filename) or False == os.access(nginx_filename, os.W_OK) ):
logger.error('%s not writeable' % nginx_filename)
sys.exit(1)
try:
nginx_file = open(nginx_filename, 'w')
except IOError:
logger.error('could not open %s' % nginx_filename)
nginx_file.write('user http;\n'
'worker_processes 2;\n'
'events {\n'
' worker_connections 1024;\n'
'}'
'http {\n'
' server {\n'
' listen [::]:80 default_server;\n'
' listen 0.0.0.0:80 default_server;\n'
' server_name %s;\n'
' server_name_in_redirect off;\n'
' location / {\n'
' root %s;\n'
' }\n'
' }\n'
' server {\n'
' listen [::]:443 default_server spdy;\n'
' listen 0.0.0.0:443 default_server spdy;\n'
' server_name %s;\n'
' server_name_in_redirect off;\n'
' ssl on;\n'
' ssl_certificate %s;\n'
' ssl_certificate_key %s;\n'
' ssl_session_timeout 5m;\n'
' ssl_session_cache shared:SSL:5m;\n'
' ssl_protocols TLSv1.2;\n'
' ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384;\n'
' ssl_prefer_server_ciphers on;\n'
' location / {\n'
' root %s;\n'
' }\n'
' }\n'
'}\n' % (config['domain'], config['letsencrypt']['webroot-path'], config['domain'], '/tmp/webroot.pem', config['filenames']['next']['key'], config['letsencrypt']['webroot-path']))
nginx_file.close()
def start_nginx(logger, config):
check_directory(logger, config['letsencrypt']['webroot-path'], False)
try:
subprocess.check_call(['bash', '-c', 'nginx -c /tmp/webroot.conf &'])
except subprocess.CalledProcessError as e :
logger.error('unable to start nginx (return %d)' % e.returncode)
sys.exit(1)
def stop_nginx(logger, config):
try:
subprocess.check_call(['pkill', 'nginx'])
except subprocess.CalledProcessError as e :
logger.error('unable to stop nginx (return %d)' % e.returncode)
os.remove('/tmp/webroot.pem')
os.remove('/tmp/webroot.conf')
# shutil.rmtree('/tmp/webroot/')
if '__main__' == __name__:
parser = argparse.ArgumentParser();
parser.add_argument('--conf', help='set configuration file', dest='conf', metavar='CONFIG_FILE', type=argparse.FileType('r'), nargs=1, required=False, default='/etc/letsencrypt/letsencrypt-wrapper.conf');
parser.add_argument('-d', '--domain', help='set domain', dest='domain', metavar='DOMAIN', type=str, nargs=1, required=True);
parser.add_argument('-e', '--other-domains', help='set other domains', dest='other_domain', metavar='OTHER_DOMAIN', type=str, nargs=1, required=False);
parser.add_argument('-c', '--chain', help='set certification chain', dest='chain', metavar='CHAIN', type=str, nargs=1, required=True);
parser.add_argument('-v', help='set loglevel to debug', dest='verbose', action='store_true');
args = parser.parse_args();
logger = logging.getLogger('logger');
logger.setLevel(logging.DEBUG if args.verbose else logging.ERROR)
ch = logging.StreamHandler(sys.stderr)
ch.setLevel(logging.DEBUG if args.verbose else logging.ERROR)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
# parse configuration
logger.info('parse configuration')
try:
if type(args.conf) is list:
user_config = json.loads(args.conf[0].read());
else:
user_config = json.loads(args.conf.read());
except :
logger.error('invalid json');
sys.exit(1);
# check
if args.chain[0] not in user_config.keys():
logger.error('chain %s is not configured' % args.chain[0] )
sys.exit(1)
if 'server' not in user_config[args.chain[0]]:
logger.error('server unknown for chain %s' % args.chain[0] )
sys.exit(1)
if 'email' not in user_config[args.chain[0]]:
logger.error('email unknown for chain %s' % args.chain[0] )
sys.exit(1)
if 'type' not in user_config[args.chain[0]]:
logger.error('type unknown for chain %s' % args.chain[0] )
sys.exit(1)
if user_config[args.chain[0]]['type'] not in ['webroot', 'standalone']:
logger.error('type %s is unknown' % user_config[args.chain[0]]['type'])
sys.exit(1)
if 'webroot' == user_config[args.chain[0]]['type'] and 'launch-nginx' not in user_config[args.chain[0]]:
logger.error('launch-nginx unknown for chain %s' % args.chain[0] )
sys.exit(1)
if 'webroot' == user_config[args.chain[0]]['type'] and 'webroot-path' not in user_config[args.chain[0]]:
user_config[args.chain[0]]['webroot-path'] = '/tmp/webroot/'
directory = '/etc/letsencrypt/%s/' % args.chain[0]
config = {
'domain': args.domain[0],
'other_domains': args.other_domain,
'chain': args.chain[0],
'directory': directory,
'cn': ('/O=confais.org/CN=%s' % args.domain[0]),
'letsencrypt': user_config[args.chain[0]],
'filenames': {
'cli': '%s/cli.ini' % directory,
'next': {
'key': '%s/keys/%s.next.key' % (directory, args.domain[0]),
'csr': {
'pem': '%s/csr/%s.next.pem.csr' % (directory, args.domain[0]),
'der': '%s/csr/%s.next.der.csr' % (directory, args.domain[0])
}
},
'current': {
'key': '%s/keys/%s.key' % (directory, args.domain[0]),
'csr': '%s/csr/%s.csr' % (directory, args.domain[0])
},
'live': {
'cert': '%s/live/%s/cert.pem' % (directory, args.domain[0]),
'sct': '%s/live/%s/cert.sct' % (directory, args.domain[0]),
'chain': '%s/live/%s/issuer.pem' % (directory, args.domain[0]),
'fullchain': '%s/live/%s/chain.pem' % (directory, args.domain[0]),
'fullchain_with_root': '%s/live/%s/fullchain.pem' % (directory, args.domain[0]),
'dh': '%s/live/%s/dh.' % (directory, args.domain[0])
},
'archive': {
'key': '%s/archive/%s.key' % (directory, args.domain[0]),
'cert': '%s/archive/%s.pem' % (directory, args.domain[0])
}
}
}
check_directory(logger, config['directory'], True);
os.chdir(config['directory'])
for subdir in ['accounts', 'archive', 'csr', 'keys', 'live', 'renewal', ('live/%s' % config['domain']) ]:
check_directory(logger, '%s/%s' % (config['directory'], subdir), False);
# create config file
logger.info('create config file')
create_config_file(logger, config)
# check if a next key is ready
logger.info('create the key if it doesn\'t exist')
create_next_key(logger, config)
# generate csr
logger.info('generate the csr')
create_next_csr(logger, config)
# sign using certbot
if 'webroot' == config['letsencrypt']['type'] and True == config['letsencrypt']['launch-nginx']:
logger.info('create a nginx configuration')
autosign(logger, config)
create_nginx_config_file(logger, config)
start_nginx(logger, config)
logger.info('sign with certbot')
try:
subprocess.check_call(['certbot', '-c', config['filenames']['cli'], '--csr', config['filenames']['next']['csr']['der'], 'certonly'])
except subprocess.CalledProcessError as e :
logger.error('certbot signature failed (return %d)' % e.returncode)
if 'webroot' == config['letsencrypt']['type'] and True == config['letsencrypt']['launch-nginx']:
logger.info('stop nginx')
stop_nginx(logger, config)
sys.exit(1)
if 'webroot' == config['letsencrypt']['type'] and True == config['letsencrypt']['launch-nginx']:
logger.info('stop nginx')
stop_nginx(logger, config)
logger.info('move files')
move_files(logger, config)
# generate dh parameters
logger.info('create dh files')
create_dh(logger, config)
# print some informations
logger.info('some informations')
info(logger, config)