|
#!/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)
|
|
|