2015-07-12 19:22:10 +00:00
# coding: utf-8
2015-08-13 08:11:08 +00:00
import click
2015-10-17 15:07:26 +00:00
import hashlib
2015-08-13 08:11:08 +00:00
import logging
2015-07-12 19:22:10 +00:00
import os
2016-09-17 21:00:14 +00:00
import random
2018-04-15 19:27:22 +00:00
import re
2015-07-26 20:34:46 +00:00
import signal
2016-09-17 21:00:14 +00:00
import string
2015-07-27 15:49:50 +00:00
import subprocess
2015-08-13 08:11:08 +00:00
import sys
2017-12-30 13:57:48 +00:00
from asn1crypto import pem , x509
2018-04-15 19:27:22 +00:00
from asn1crypto . csr import CertificationRequest
2018-04-27 07:48:15 +00:00
from asn1crypto . crl import CertificateList
2017-05-27 18:17:21 +00:00
from base64 import b64encode
2017-12-30 13:57:48 +00:00
from certbuilder import CertificateBuilder , pem_armor_certificate
from certidude import const
from csrbuilder import CSRBuilder , pem_armor_csr
2018-01-05 19:55:15 +00:00
from configparser import ConfigParser , NoOptionError
2018-05-15 07:45:29 +00:00
from certidude . common import apt , rpm , drop_privileges , selinux_fixup , cn_to_dn , generate_serial
2016-03-29 16:29:06 +00:00
from datetime import datetime , timedelta
2017-12-30 13:57:48 +00:00
from glob import glob
from ipaddress import ip_network
from oscrypto import asymmetric
2018-05-17 09:00:13 +00:00
try :
import coverage
cov = coverage . process_startup ( )
if cov :
click . echo ( " Enabling coverage tracking " )
else :
click . echo ( " Coverage tracking not requested " )
except ImportError :
pass
2017-04-04 05:02:08 +00:00
logger = logging . getLogger ( __name__ )
2015-07-26 20:34:46 +00:00
2015-07-12 19:22:10 +00:00
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
2015-07-26 20:34:46 +00:00
# https://kjur.github.io/jsrsasign/
2016-09-17 21:00:14 +00:00
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
2015-08-13 08:11:08 +00:00
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
2015-07-12 19:22:10 +00:00
2017-08-16 20:25:16 +00:00
NOW = datetime . utcnow ( )
2015-08-13 08:11:08 +00:00
2017-05-04 06:40:47 +00:00
def fqdn_required ( func ) :
def wrapped ( * * args ) :
common_name = args . get ( " common_name " )
if " . " in common_name :
2017-12-30 13:57:48 +00:00
logger . info ( " Using fully qualified hostname %s " % common_name )
2017-05-04 06:40:47 +00:00
else :
raise ValueError ( " Fully qualified hostname not specified as common name, make sure hostname -f works " )
return func ( * * args )
return wrapped
2017-05-03 21:12:51 +00:00
def setup_client ( prefix = " client_ " , dh = False ) :
2017-05-03 21:03:51 +00:00
# Create section in /etc/certidude/client.conf
def wrapper ( func ) :
def wrapped ( * * arguments ) :
common_name = arguments . get ( " common_name " )
authority = arguments . get ( " authority " )
2018-04-27 07:48:15 +00:00
b = os . path . join ( " /etc/certidude/authority " , authority )
2017-05-03 21:12:51 +00:00
if dh :
2018-05-15 07:45:29 +00:00
path = os . path . join ( " /etc/ssl/dhparam.pem " )
2017-05-03 21:12:51 +00:00
if not os . path . exists ( path ) :
rpm ( " openssl " )
apt ( " openssl " )
2018-05-17 09:00:13 +00:00
cmd = " openssl " , " dhparam " , " -out " , path , str ( const . KEY_SIZE )
2017-05-03 21:12:51 +00:00
subprocess . check_call ( cmd )
arguments [ " dhparam_path " ] = path
2017-05-03 21:03:51 +00:00
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser ( )
if os . path . exists ( const . CLIENT_CONFIG_PATH ) :
client_config . readfp ( open ( const . CLIENT_CONFIG_PATH ) )
if client_config . has_section ( authority ) :
click . echo ( " Section ' %s ' already exists in %s , remove to regenerate " % ( authority , const . CLIENT_CONFIG_PATH ) )
else :
client_config . add_section ( authority )
client_config . set ( authority , " trigger " , " interface up " )
client_config . set ( authority , " common name " , common_name )
client_config . set ( authority , " request path " , os . path . join ( b , prefix + " req.pem " ) )
client_config . set ( authority , " key path " , os . path . join ( b , prefix + " key.pem " ) )
client_config . set ( authority , " certificate path " , os . path . join ( b , prefix + " cert.pem " ) )
client_config . set ( authority , " authority path " , os . path . join ( b , " ca_cert.pem " ) )
client_config . set ( authority , " revocations path " , os . path . join ( b , " ca_crl.pem " ) )
2017-12-30 13:57:48 +00:00
with open ( const . CLIENT_CONFIG_PATH + " .part " , ' w ' ) as fh :
2017-05-03 21:03:51 +00:00
client_config . write ( fh )
os . rename ( const . CLIENT_CONFIG_PATH + " .part " , const . CLIENT_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( authority , const . CLIENT_CONFIG_PATH ) )
for j in ( " key " , " request " , " certificate " , " authority " , " revocations " ) :
arguments [ " %s _path " % j ] = client_config . get ( authority , " %s path " % j )
return func ( * * arguments )
return wrapped
return wrapper
2017-05-03 21:12:51 +00:00
2018-05-04 06:55:01 +00:00
class ConfigTreeParser ( ConfigParser ) :
def __init__ ( self , path , * args , * * kwargs ) :
ConfigParser . __init__ ( self , * args , * * kwargs )
if os . path . exists ( path ) :
with open ( path ) as fh :
click . echo ( " Parsing: %s " % fh . name )
self . readfp ( fh )
if os . path . exists ( path + " .d " ) :
for filename in os . listdir ( path + " .d " ) :
if not filename . endswith ( " .conf " ) :
continue
with open ( os . path . join ( path + " .d " , filename ) ) as fh :
click . echo ( " Parsing: %s " % fh . name )
self . readfp ( fh )
2017-12-30 13:57:48 +00:00
@click.command ( " enroll " , help = " Run processes for requesting certificates and configuring services " )
2017-06-04 14:19:29 +00:00
@click.option ( " -k " , " --kerberos " , default = False , is_flag = True , help = " Offer system keytab for auth " )
2017-03-13 17:47:58 +00:00
@click.option ( " -r " , " --renew " , default = False , is_flag = True , help = " Renew now " )
2016-01-14 22:47:30 +00:00
@click.option ( " -f " , " --fork " , default = False , is_flag = True , help = " Fork to background " )
2017-12-30 13:57:48 +00:00
@click.option ( " -s " , " --skip-self " , default = False , is_flag = True , help = " Skip self enroll " )
2017-05-01 16:20:50 +00:00
@click.option ( " -nw " , " --no-wait " , default = False , is_flag = True , help = " Return immideately if server doesn ' t autosign " )
2017-12-30 13:57:48 +00:00
def certidude_enroll ( fork , renew , no_wait , kerberos , skip_self ) :
2018-04-27 07:48:15 +00:00
assert os . getuid ( ) == 0 and os . getgid ( ) == 0 , " Can enroll only as root "
2018-01-03 22:12:02 +00:00
if not skip_self and os . path . exists ( const . SERVER_CONFIG_PATH ) :
2017-12-30 13:57:48 +00:00
click . echo ( " Self-enrolling authority ' s web interface certificate " )
from certidude import authority
authority . self_enroll ( )
2017-05-06 21:07:41 +00:00
2017-05-01 16:20:50 +00:00
from jinja2 import Environment , PackageLoader
2017-12-30 13:57:48 +00:00
context = globals ( )
context . update ( locals ( ) )
2017-05-01 16:20:50 +00:00
env = Environment ( loader = PackageLoader ( " certidude " , " templates " ) , trim_blocks = True )
2017-12-30 13:57:48 +00:00
if not os . path . exists ( " /etc/systemd/system/certidude-enroll.timer " ) :
click . echo ( " Creating systemd timer... " )
with open ( " /etc/systemd/system/certidude-enroll.timer " , " w " ) as fh :
fh . write ( env . get_template ( " client/certidude.timer " ) . render ( context ) )
if not os . path . exists ( " /etc/systemd/system/certidude-enroll.service " ) :
click . echo ( " Creating systemd service... " )
with open ( " /etc/systemd/system/certidude-enroll.service " , " w " ) as fh :
fh . write ( env . get_template ( " client/certidude.service " ) . render ( context ) )
os . system ( " systemctl daemon-reload " )
os . system ( " systemctl enable certidude-enroll.timer " )
os . system ( " systemctl start certidude-enroll.timer " )
2017-04-13 20:30:28 +00:00
2016-09-17 21:00:14 +00:00
if not os . path . exists ( const . CLIENT_CONFIG_PATH ) :
2017-12-30 13:57:48 +00:00
click . echo ( " Client not configured, so not going to enroll " )
return
import requests
2016-09-17 21:00:14 +00:00
2018-05-04 06:55:01 +00:00
clients = ConfigTreeParser ( const . CLIENT_CONFIG_PATH )
service_config = ConfigTreeParser ( const . SERVICES_CONFIG_PATH )
2016-01-14 22:47:30 +00:00
# Process directories
2017-05-01 16:20:50 +00:00
if not os . path . exists ( const . RUN_DIR ) :
click . echo ( " Creating: %s " % const . RUN_DIR )
os . makedirs ( const . RUN_DIR )
2016-01-14 22:47:30 +00:00
2017-05-27 18:17:21 +00:00
for authority_name in clients . sections ( ) :
2016-09-17 21:00:14 +00:00
# TODO: Create directories automatically
2017-05-27 18:17:21 +00:00
try :
trigger = clients . get ( authority_name , " trigger " )
except NoOptionError :
trigger = " interface up "
2016-01-14 22:47:30 +00:00
2017-05-27 18:17:21 +00:00
if trigger == " domain joined " :
2017-05-08 16:25:59 +00:00
# Stop further processing if command line argument said so or trigger expects domain membership
if not os . path . exists ( " /etc/krb5.keytab " ) :
continue
2017-06-04 14:19:29 +00:00
kerberos = True
2017-05-27 18:17:21 +00:00
elif trigger == " interface up " :
pass
else :
raise
#########################
### Fork if requested ###
#########################
2017-05-08 16:25:59 +00:00
2017-05-27 18:17:21 +00:00
pid_path = os . path . join ( const . RUN_DIR , authority_name + " .pid " )
2016-01-14 22:47:30 +00:00
try :
with open ( pid_path ) as fh :
pid = int ( fh . readline ( ) )
os . kill ( pid , signal . SIGTERM )
click . echo ( " Terminated process %d " % pid )
os . unlink ( pid_path )
2016-03-21 21:42:39 +00:00
except EnvironmentError :
2016-01-14 22:47:30 +00:00
pass
if fork :
child_pid = os . fork ( )
else :
child_pid = None
if child_pid :
click . echo ( " Spawned certificate request process with PID %d " % ( child_pid ) )
continue
with open ( pid_path , " w " ) as fh :
fh . write ( " %d \n " % os . getpid ( ) )
2016-09-17 21:00:14 +00:00
2017-05-27 18:17:21 +00:00
try :
authority_path = clients . get ( authority_name , " authority path " )
except NoOptionError :
2018-04-27 07:48:15 +00:00
authority_path = " /etc/certidude/authority/ %s /ca_cert.pem " % authority_name
2017-05-27 18:17:21 +00:00
finally :
if os . path . exists ( authority_path ) :
click . echo ( " Found authority certificate in: %s " % authority_path )
2018-04-10 09:29:05 +00:00
with open ( authority_path , " rb " ) as fh :
header , _ , certificate_der_bytes = pem . unarmor ( fh . read ( ) )
authority_certificate = x509 . Certificate . load ( certificate_der_bytes )
2017-05-27 18:17:21 +00:00
else :
if not os . path . exists ( os . path . dirname ( authority_path ) ) :
os . makedirs ( os . path . dirname ( authority_path ) )
2018-04-15 19:27:22 +00:00
authority_url = " http:// %s /api/certificate/ " % authority_name
2017-05-27 18:17:21 +00:00
click . echo ( " Attempting to fetch authority certificate from %s " % authority_url )
try :
r = requests . get ( authority_url ,
headers = { " Accept " : " application/x-x509-ca-cert,application/x-pem-file " } )
2017-12-30 13:57:48 +00:00
header , _ , certificate_der_bytes = pem . unarmor ( r . content )
2018-04-10 09:29:05 +00:00
authority_certificate = x509 . Certificate . load ( certificate_der_bytes )
2017-12-30 13:57:48 +00:00
except : # TODO: catch correct exceptions
2017-05-27 18:17:21 +00:00
raise
# raise ValueError("Failed to parse PEM: %s" % r.text)
authority_partial = authority_path + " .part "
2017-12-30 13:57:48 +00:00
with open ( authority_partial , " wb " ) as oh :
2017-05-27 18:17:21 +00:00
oh . write ( r . content )
click . echo ( " Writing authority certificate to: %s " % authority_path )
selinux_fixup ( authority_partial )
os . rename ( authority_partial , authority_path )
2018-04-10 09:29:05 +00:00
authority_public_key = asymmetric . load_public_key (
authority_certificate [ " tbs_certificate " ] [ " subject_public_key_info " ] )
2017-05-27 18:17:21 +00:00
# Attempt to install CA certificates system wide
try :
authority_system_wide = clients . getboolean ( authority_name , " system wide " )
except NoOptionError :
authority_system_wide = False
finally :
if authority_system_wide :
# Firefox, Chromium, wget, curl on Fedora
# Note that if ~/.pki/nssdb has been customized before, curl breaks
if os . path . exists ( " /usr/bin/update-ca-trust " ) :
link_path = " /etc/pki/ca-trust/source/anchors/ %s " % authority_name
if not os . path . lexists ( link_path ) :
os . symlink ( authority_path , link_path )
os . system ( " update-ca-trust " )
# curl on Fedora ?
# pip
2017-07-06 09:29:02 +00:00
# Firefox (?) on Debian, Ubuntu
2018-04-27 07:48:15 +00:00
if os . path . exists ( " /usr/bin/update-ca-certificates " ) or os . path . exists ( " /usr/sbin/update-ca-certificates " ) :
2017-07-06 09:29:02 +00:00
link_path = " /usr/local/share/ca-certificates/ %s " % authority_name
if not os . path . lexists ( link_path ) :
os . symlink ( authority_path , link_path )
os . system ( " update-ca-certificates " )
# TODO: test for curl, wget
2017-05-27 18:17:21 +00:00
###############
### Get CRL ###
###############
try :
revocations_path = clients . get ( authority_name , " revocations path " )
except NoOptionError :
revocations_path = None
else :
# Fetch certificate revocation list
2018-04-15 19:27:22 +00:00
revoked_url = " http:// %s /api/revoked/ " % authority_name
2017-05-27 18:17:21 +00:00
click . echo ( " Fetching CRL from %s to %s " % ( revoked_url , revocations_path ) )
r = requests . get ( revoked_url , headers = { ' accept ' : ' application/x-pem-file ' } )
2018-04-15 19:27:22 +00:00
if r . status_code == 200 :
2018-04-27 07:48:15 +00:00
header , _ , crl_der_bytes = pem . unarmor ( r . content )
revocations = CertificateList . load ( crl_der_bytes )
2018-04-15 19:27:22 +00:00
# TODO: check signature, parse reasons, remove keys if revoked
revocations_partial = revocations_path + " .part "
with open ( revocations_partial , ' wb ' ) as f :
f . write ( r . content )
2018-04-27 07:48:15 +00:00
os . rename ( revocations_partial , revocations_path )
2018-04-15 19:27:22 +00:00
elif r . status_code == 404 :
click . echo ( " CRL disabled, server said 404 " )
else :
click . echo ( " Failed to fetch CRL from %s , got %s " % ( revoked_url , r . text ) )
2017-05-27 18:17:21 +00:00
try :
common_name = clients . get ( authority_name , " common name " )
except NoOptionError :
click . echo ( " No common name specified for %s , not requesting a certificate " % authority_name )
continue
2018-03-22 09:15:11 +00:00
# If deriving common name from *current* hostname is preferred
if common_name == " $HOSTNAME " :
common_name = const . HOSTNAME
2018-04-15 19:27:22 +00:00
elif common_name == " $FQDN " :
common_name = const . FQDN
elif " $ " in common_name :
raise ValueError ( " Invalid variable ' %s ' supplied, only $HOSTNAME and $FQDN allowed " % common_name )
2018-04-16 12:13:31 +00:00
if not re . match ( const . RE_COMMON_NAME , common_name ) :
raise ValueError ( " Supplied common name %s doesn ' t match the expression %s " % ( common_name , const . RE_COMMON_NAME ) )
2018-04-15 19:27:22 +00:00
2018-03-22 09:15:11 +00:00
2017-05-27 18:17:21 +00:00
################################
### Generate keypair and CSR ###
################################
try :
key_path = clients . get ( authority_name , " key path " )
request_path = clients . get ( authority_name , " request path " )
except NoOptionError :
2018-04-27 07:48:15 +00:00
key_path = " /etc/certidude/authority/ %s /host_key.pem " % authority_name
request_path = " /etc/certidude/authority/ %s /host_csr.pem " % authority_name
2017-05-27 18:17:21 +00:00
2018-04-15 19:27:22 +00:00
if os . path . exists ( request_path ) :
with open ( request_path , " rb " ) as fh :
header , _ , der_bytes = pem . unarmor ( fh . read ( ) )
csr = CertificationRequest . load ( der_bytes )
if csr [ " certification_request_info " ] [ " subject " ] . native [ " common_name " ] != common_name :
click . echo ( " Stored request ' s common name differs from currently requested one, deleting old request " )
os . remove ( request_path )
2017-05-27 18:17:21 +00:00
if not os . path . exists ( request_path ) :
key_partial = key_path + " .part "
request_partial = request_path + " .part "
2018-04-09 13:08:12 +00:00
2018-04-10 09:29:05 +00:00
if authority_public_key . algorithm == " ec " :
self_public_key , private_key = asymmetric . generate_pair ( " ec " , curve = authority_public_key . curve )
elif authority_public_key . algorithm == " rsa " :
self_public_key , private_key = asymmetric . generate_pair ( " rsa " , bit_size = authority_public_key . bit_size )
2018-04-09 13:08:12 +00:00
else :
NotImplemented
2018-04-10 09:29:05 +00:00
builder = CSRBuilder ( { " common_name " : common_name } , self_public_key )
2017-05-27 18:17:21 +00:00
request = builder . build ( private_key )
with open ( key_partial , ' wb ' ) as f :
f . write ( asymmetric . dump_private_key ( private_key , None ) )
with open ( request_partial , ' wb ' ) as f :
f . write ( pem_armor_csr ( request ) )
selinux_fixup ( key_partial )
selinux_fixup ( request_partial )
os . rename ( key_partial , key_path )
os . rename ( request_partial , request_path )
2018-04-15 19:27:22 +00:00
2017-05-27 18:17:21 +00:00
##############################################
### Submit CSR and save signed certificate ###
##############################################
try :
certificate_path = clients . get ( authority_name , " certificate path " )
except NoOptionError :
2018-04-27 07:48:15 +00:00
certificate_path = " /etc/certidude/authority/ %s /host_cert.pem " % authority_name
2017-05-27 18:17:21 +00:00
try :
renewal_overlap = clients . getint ( authority_name , " renewal overlap " )
2018-04-16 12:13:31 +00:00
except NoOptionError : # Renewal not configured
2017-06-04 14:19:29 +00:00
renewal_overlap = None
try :
with open ( certificate_path , " rb " ) as ch , open ( request_path , " rb " ) as rh , open ( key_path , " rb " ) as kh :
2017-05-27 18:17:21 +00:00
cert_buf = ch . read ( )
cert = asymmetric . load_certificate ( cert_buf )
2017-08-16 20:25:16 +00:00
expires = cert . asn1 [ " tbs_certificate " ] [ " validity " ] [ " not_after " ] . native . replace ( tzinfo = None )
if renewal_overlap and NOW > expires - timedelta ( days = renewal_overlap ) :
2017-05-27 18:17:21 +00:00
click . echo ( " Certificate will expire %s , will attempt to renew " % expires )
renew = True
2017-06-04 14:19:29 +00:00
except EnvironmentError : # Certificate missing, can't renew
2017-05-27 18:17:21 +00:00
pass
2018-04-27 07:48:15 +00:00
try :
autosign = clients . getboolean ( authority_name , " autosign " )
except NoOptionError :
autosign = True
2017-05-27 18:17:21 +00:00
if not os . path . exists ( certificate_path ) or renew :
# Set up URL-s
request_params = set ( )
2018-04-27 07:48:15 +00:00
request_params . add ( " autosign= %s " % ( " yes " if autosign else " no " ) )
2017-05-27 18:17:21 +00:00
if not no_wait :
request_params . add ( " wait=forever " )
2018-04-15 19:27:22 +00:00
kwargs = {
" data " : open ( request_path ) ,
" verify " : authority_path ,
" headers " : {
" Content-Type " : " application/pkcs10 " ,
" Accept " : " application/x-x509-user-cert,application/x-pem-file "
}
}
if renew : # Do mutually authenticated TLS handshake
kwargs [ " cert " ] = certificate_path , key_path
2018-04-27 07:48:15 +00:00
click . echo ( " Renewing using current keypair at %s %s " % kwargs [ " cert " ] )
2017-05-27 18:17:21 +00:00
else :
2018-04-15 19:27:22 +00:00
# If machine is joined to domain attempt to present machine credentials for authentication
if kerberos :
try :
from requests_kerberos import HTTPKerberosAuth , OPTIONAL
except ImportError :
click . echo ( " Kerberos bindings not available, please install requests-kerberos " )
else :
os . environ [ " KRB5CCNAME " ] = " /tmp/ca.ticket "
# Mac OS X has keytab with lowercase hostname
cmd = " kinit -S HTTP/ %s -k %s $ " % ( authority_name , const . HOSTNAME . lower ( ) )
click . echo ( " Executing: %s " % cmd )
if os . system ( cmd ) :
# Fedora /w SSSD has keytab with uppercase hostname
cmd = " kinit -S HTTP/ %s -k %s $ " % ( authority_name , const . HOSTNAME . upper ( ) )
if os . system ( cmd ) :
# Failed, probably /etc/krb5.keytab contains spaghetti
raise ValueError ( " Failed to initialize Kerberos service ticket using machine keytab " )
assert os . path . exists ( " /tmp/ca.ticket " ) , " Ticket not created! "
click . echo ( " Initialized Kerberos service ticket using machine keytab " )
kwargs [ " auth " ] = HTTPKerberosAuth ( mutual_authentication = OPTIONAL , force_preemptive = True )
else :
click . echo ( " Not using machine keytab " )
2017-05-27 18:17:21 +00:00
2018-05-15 07:45:29 +00:00
request_url = " https:// %s :8443/api/request/ " % authority_name
2018-04-15 19:27:22 +00:00
if request_params :
request_url = request_url + " ? " + " & " . join ( request_params )
submission = requests . post ( request_url , * * kwargs )
2017-05-27 18:17:21 +00:00
# Destroy service ticket
if os . path . exists ( " /tmp/ca.ticket " ) :
os . system ( " kdestroy " )
if submission . status_code == requests . codes . ok :
pass
if submission . status_code == requests . codes . accepted :
click . echo ( " Server accepted the request, but refused to sign immideately ( %s ). Waiting was not requested, hence quitting for now " % submission . text )
2017-06-04 14:19:29 +00:00
os . unlink ( pid_path )
continue
2017-05-27 18:17:21 +00:00
if submission . status_code == requests . codes . conflict :
raise errors . DuplicateCommonNameError ( " Different signing request with same CN is already present on server, server refuses to overwrite " )
elif submission . status_code == requests . codes . gone :
# Should the client retry or disable request submission?
raise ValueError ( " Server refused to sign the request " ) # TODO: Raise proper exception
2018-04-27 07:48:15 +00:00
elif submission . status_code == requests . codes . bad_request :
raise ValueError ( " Server said following, likely current certificate expired/revoked? %s " % submission . text )
2017-05-27 18:17:21 +00:00
else :
submission . raise_for_status ( )
2016-01-14 22:47:30 +00:00
try :
2017-12-30 13:57:48 +00:00
header , _ , certificate_der_bytes = pem . unarmor ( submission . content )
cert = x509 . Certificate . load ( certificate_der_bytes )
2017-05-27 18:17:21 +00:00
except : # TODO: catch correct exceptions
raise ValueError ( " Failed to parse PEM: %s " % submission . text )
2017-12-30 13:57:48 +00:00
assert cert . subject . native [ " common_name " ] == common_name , \
" Expected certificate with common name %s , but got %s instead " % \
( common_name , cert . subject . native [ " common_name " ] )
2017-05-27 18:17:21 +00:00
os . umask ( 0o022 )
certificate_partial = certificate_path + " .part "
with open ( certificate_partial , " w " ) as fh :
# Dump certificate
fh . write ( submission . text )
click . echo ( " Writing certificate to: %s " % certificate_path )
selinux_fixup ( certificate_partial )
os . rename ( certificate_partial , certificate_path )
# Nginx requires bundle
try :
bundle_path = clients . get ( authority_name , " bundle path " )
except NoOptionError :
pass
else :
bundle_partial = bundle_path + " .part "
with open ( bundle_partial , " w " ) as fh :
fh . write ( submission . text )
with open ( authority_path ) as ch :
fh . write ( ch . read ( ) )
click . echo ( " Writing bundle to: %s " % bundle_path )
os . rename ( bundle_partial , bundle_path )
2017-12-30 13:57:48 +00:00
else :
click . echo ( " Certificate found at %s and no renewal requested " % certificate_path )
2017-05-27 18:17:21 +00:00
##################################
### Configure related services ###
##################################
2016-01-14 22:47:30 +00:00
2016-09-17 21:00:14 +00:00
for endpoint in service_config . sections ( ) :
2017-05-27 18:17:21 +00:00
if service_config . get ( endpoint , " authority " ) != authority_name :
2016-01-14 22:47:30 +00:00
continue
2016-09-17 21:00:14 +00:00
click . echo ( " Configuring ' %s ' " % endpoint )
2016-01-14 22:47:30 +00:00
csummer = hashlib . sha1 ( )
csummer . update ( endpoint . encode ( " ascii " ) )
csum = csummer . hexdigest ( )
uuid = csum [ : 8 ] + " - " + csum [ 8 : 12 ] + " - " + csum [ 12 : 16 ] + " - " + csum [ 16 : 20 ] + " - " + csum [ 20 : 32 ]
2016-03-27 20:38:14 +00:00
# Intranet HTTPS handled by PKCS#12 bundle generation,
# so it will not be implemented here
2016-09-17 21:00:14 +00:00
# OpenVPN set up with initscripts
if service_config . get ( endpoint , " service " ) == " init/openvpn " :
if os . path . exists ( " /etc/openvpn/ %s .disabled " % endpoint ) and not os . path . exists ( " /etc/openvpn/ %s .conf " % endpoint ) :
os . rename ( " /etc/openvpn/ %s .disabled " % endpoint , " /etc/openvpn/ %s .conf " % endpoint )
if os . path . exists ( " /bin/systemctl " ) :
click . echo ( " Re-running systemd generators for OpenVPN... " )
os . system ( " systemctl daemon-reload " )
2017-04-29 19:09:31 +00:00
if not os . path . exists ( " /etc/systemd/system/openvpn-reconnect.service " ) :
2017-12-30 18:56:12 +00:00
with open ( " /etc/systemd/system/openvpn-reconnect.service.part " , " w " ) as fh :
2017-04-29 19:09:31 +00:00
fh . write ( env . get_template ( " client/openvpn-reconnect.service " ) . render ( context ) )
2017-12-30 18:56:12 +00:00
os . rename ( " /etc/systemd/system/openvpn-reconnect.service.part " ,
" /etc/systemd/system/openvpn-reconnect.service " )
2017-04-29 19:09:31 +00:00
click . echo ( " Created /etc/systemd/system/openvpn-reconnect.service " )
2016-09-17 21:00:14 +00:00
click . echo ( " Starting OpenVPN... " )
os . system ( " service openvpn start " )
2016-01-14 22:47:30 +00:00
continue
2016-09-17 21:00:14 +00:00
# IPSec set up with initscripts
if service_config . get ( endpoint , " service " ) == " init/strongswan " :
2016-01-14 22:47:30 +00:00
from ipsecparse import loads
2017-04-13 23:49:11 +00:00
config = loads ( open ( " %s /ipsec.conf " % const . STRONGSWAN_PREFIX ) . read ( ) )
2017-04-14 17:21:31 +00:00
for section_type , section_name in config :
# Identify correct ipsec.conf section by leftcert
if section_type != " conn " :
continue
2017-05-27 18:17:21 +00:00
if config [ section_type , section_name ] [ " leftcert " ] != certificate_path :
2017-04-14 17:21:31 +00:00
continue
2017-05-03 14:42:37 +00:00
if config [ section_type , section_name ] . get ( " left " , " " ) == " %d efaultroute " :
2017-04-14 17:21:31 +00:00
config [ section_type , section_name ] [ " auto " ] = " start " # This is client
2017-05-03 14:42:37 +00:00
elif config [ section_type , section_name ] . get ( " leftsourceip " , " " ) :
2017-04-14 17:21:31 +00:00
config [ section_type , section_name ] [ " auto " ] = " add " # This is server
else :
config [ section_type , section_name ] [ " auto " ] = " route " # This is site-to-site tunnel
with open ( " %s /ipsec.conf.part " % const . STRONGSWAN_PREFIX , " w " ) as fh :
fh . write ( config . dumps ( ) )
os . rename (
" %s /ipsec.conf.part " % const . STRONGSWAN_PREFIX ,
" %s /ipsec.conf " % const . STRONGSWAN_PREFIX )
break
2016-01-14 22:47:30 +00:00
2017-12-30 13:57:48 +00:00
# Tune AppArmor profile, TODO: retain contents
if os . path . exists ( " /etc/apparmor.d/local " ) :
with open ( " /etc/apparmor.d/local/usr.lib.ipsec.charon " , " w " ) as fh :
fh . write ( key_path + " r, \n " )
fh . write ( authority_path + " r, \n " )
fh . write ( certificate_path + " r, \n " )
2016-01-14 22:47:30 +00:00
# Attempt to reload config or start if it's not running
2017-04-13 23:49:11 +00:00
if os . path . exists ( " /usr/sbin/strongswan " ) : # wtf fedora
2017-04-14 17:21:31 +00:00
if os . system ( " strongswan update " ) :
2017-04-13 23:49:11 +00:00
os . system ( " strongswan start " )
else :
2017-04-14 17:21:31 +00:00
if os . system ( " ipsec update " ) :
2017-04-13 23:49:11 +00:00
os . system ( " ipsec start " )
2016-01-14 22:47:30 +00:00
continue
2016-09-17 21:00:14 +00:00
# OpenVPN set up with NetworkManager
if service_config . get ( endpoint , " service " ) == " network-manager/openvpn " :
2017-04-13 15:17:05 +00:00
# NetworkManager-strongswan-gnome
2017-03-13 11:42:58 +00:00
nm_config_path = os . path . join ( " /etc/NetworkManager/system-connections " , endpoint )
if os . path . exists ( nm_config_path ) :
click . echo ( " Not creating %s , remove to regenerate " % nm_config_path )
continue
2016-09-17 21:00:14 +00:00
nm_config = ConfigParser ( )
nm_config . add_section ( " connection " )
2017-05-27 18:17:21 +00:00
nm_config . set ( " connection " , " certidude managed " , " true " )
2016-09-17 21:00:14 +00:00
nm_config . set ( " connection " , " id " , endpoint )
nm_config . set ( " connection " , " uuid " , uuid )
nm_config . set ( " connection " , " type " , " vpn " )
nm_config . add_section ( " vpn " )
nm_config . set ( " vpn " , " service-type " , " org.freedesktop.NetworkManager.openvpn " )
nm_config . set ( " vpn " , " connection-type " , " tls " )
nm_config . set ( " vpn " , " comp-lzo " , " yes " )
nm_config . set ( " vpn " , " cert-pass-flags " , " 0 " )
2017-01-26 10:55:26 +00:00
nm_config . set ( " vpn " , " tap-dev " , " no " )
2016-09-17 21:00:14 +00:00
nm_config . set ( " vpn " , " remote-cert-tls " , " server " ) # Assert TLS Server flag of X.509 certificate
nm_config . set ( " vpn " , " remote " , service_config . get ( endpoint , " remote " ) )
2017-05-27 18:17:21 +00:00
nm_config . set ( " vpn " , " key " , key_path )
nm_config . set ( " vpn " , " cert " , certificate_path )
nm_config . set ( " vpn " , " ca " , authority_path )
2018-05-15 07:45:29 +00:00
nm_config . set ( " vpn " , " tls-cipher " , " TLS- %s -WITH-AES-256-GCM-SHA384 " % (
2018-05-02 08:11:01 +00:00
" ECDHE-ECDSA " if authority_public_key . algorithm == " ec " else " DHE-RSA " ) )
nm_config . set ( " vpn " , " cipher " , " AES-128-GCM " )
nm_config . set ( " vpn " , " auth " , " SHA384 " )
2016-09-17 21:00:14 +00:00
nm_config . add_section ( " ipv4 " )
nm_config . set ( " ipv4 " , " method " , " auto " )
nm_config . set ( " ipv4 " , " never-default " , " true " )
nm_config . add_section ( " ipv6 " )
nm_config . set ( " ipv6 " , " method " , " auto " )
2016-01-14 22:47:30 +00:00
2017-05-27 18:17:21 +00:00
try :
nm_config . set ( " vpn " , " port " , str ( service_config . getint ( endpoint , " port " ) ) )
except NoOptionError :
nm_config . set ( " vpn " , " port " , " 1194 " )
try :
if service_config . get ( endpoint , " proto " ) == " tcp " :
nm_config . set ( " vpn " , " proto-tcp " , " yes " )
except NoOptionError :
pass
2016-09-17 21:00:14 +00:00
# Prevent creation of files with liberal permissions
os . umask ( 0o177 )
2015-12-12 22:34:08 +00:00
2016-09-17 21:00:14 +00:00
# Write NetworkManager configuration
2017-03-13 11:42:58 +00:00
with open ( nm_config_path , " w " ) as fh :
2016-09-17 21:00:14 +00:00
nm_config . write ( fh )
2017-03-13 11:42:58 +00:00
click . echo ( " Created %s " % nm_config_path )
2017-05-03 21:03:51 +00:00
if os . path . exists ( " /run/NetworkManager " ) :
os . system ( " nmcli con reload " )
2016-09-17 21:00:14 +00:00
continue
2016-01-10 17:51:54 +00:00
2015-07-26 20:34:46 +00:00
2016-09-17 21:00:14 +00:00
# IPSec set up with NetworkManager
2017-05-27 18:17:21 +00:00
if service_config . get ( endpoint , " service " ) == " network-manager/strongswan " :
2016-09-17 21:00:14 +00:00
client_config = ConfigParser ( )
2017-04-12 13:56:29 +00:00
nm_config = ConfigParser ( )
2016-09-17 21:00:14 +00:00
nm_config . add_section ( " connection " )
2017-05-27 18:17:21 +00:00
nm_config . set ( " connection " , " certidude managed " , " true " )
2016-09-17 21:00:14 +00:00
nm_config . set ( " connection " , " id " , endpoint )
nm_config . set ( " connection " , " uuid " , uuid )
nm_config . set ( " connection " , " type " , " vpn " )
nm_config . add_section ( " vpn " )
nm_config . set ( " vpn " , " service-type " , " org.freedesktop.NetworkManager.strongswan " )
nm_config . set ( " vpn " , " encap " , " no " )
nm_config . set ( " vpn " , " virtual " , " yes " )
nm_config . set ( " vpn " , " method " , " key " )
nm_config . set ( " vpn " , " ipcomp " , " no " )
nm_config . set ( " vpn " , " address " , service_config . get ( endpoint , " remote " ) )
2017-05-27 18:17:21 +00:00
nm_config . set ( " vpn " , " userkey " , key_path )
nm_config . set ( " vpn " , " usercert " , certificate_path )
nm_config . set ( " vpn " , " certificate " , authority_path )
2018-05-02 08:11:01 +00:00
dhgroup = " ecp384 " if authority_public_key . algorithm == " ec " else " modp2048 "
nm_config . set ( " vpn " , " ike " , " aes256-sha384-prfsha384- " + dhgroup )
nm_config . set ( " vpn " , " esp " , " aes128gcm16-aes128gmac- " + dhgroup )
nm_config . set ( " vpn " , " proposal " , " yes " )
2016-09-17 21:00:14 +00:00
nm_config . add_section ( " ipv4 " )
nm_config . set ( " ipv4 " , " method " , " auto " )
2015-07-26 20:34:46 +00:00
2016-09-17 21:00:14 +00:00
# Add routes, may need some more tweaking
if service_config . has_option ( endpoint , " route " ) :
for index , subnet in enumerate ( service_config . get ( endpoint , " route " ) . split ( " , " ) , start = 1 ) :
nm_config . set ( " ipv4 " , " route %d " % index , subnet )
2015-07-26 20:34:46 +00:00
2016-09-17 21:00:14 +00:00
# Prevent creation of files with liberal permissions
os . umask ( 0o177 )
2015-12-12 22:34:08 +00:00
2016-09-17 21:00:14 +00:00
# Write NetworkManager configuration
with open ( os . path . join ( " /etc/NetworkManager/system-connections " , endpoint ) , " w " ) as fh :
nm_config . write ( fh )
click . echo ( " Created %s " % fh . name )
2017-05-03 21:03:51 +00:00
if os . path . exists ( " /run/NetworkManager " ) :
os . system ( " nmcli con reload " )
2016-09-17 21:00:14 +00:00
continue
2015-12-12 22:34:08 +00:00
2016-09-17 21:00:14 +00:00
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
2017-04-13 23:49:11 +00:00
click . echo ( " Unknown service: %s " % service_config . get ( endpoint , " service " ) )
2016-09-17 21:00:14 +00:00
os . unlink ( pid_path )
2016-01-14 22:47:30 +00:00
2015-07-26 20:34:46 +00:00
@click.command ( " server " , help = " Set up OpenVPN server " )
2016-09-17 21:00:14 +00:00
@click.argument ( " authority " )
2017-05-01 16:20:50 +00:00
@click.option ( " --common-name " , " -cn " , default = const . FQDN , help = " Common name, %s by default " % const . FQDN )
2015-07-26 20:34:46 +00:00
@click.option ( " --subnet " , " -s " , default = " 192.168.33.0/24 " , type = ip_network , help = " OpenVPN subnet, 192.168.33.0/24 by default " )
2016-03-27 20:38:14 +00:00
@click.option ( " --local " , " -l " , default = " 0.0.0.0 " , help = " OpenVPN listening address, defaults to all interfaces " )
2017-04-12 13:21:49 +00:00
@click.option ( " --port " , " -p " , default = 1194 , type = click . IntRange ( 1 , 60000 ) , help = " OpenVPN listening port, 1194 by default " )
2015-07-26 20:34:46 +00:00
@click.option ( ' --proto ' , " -t " , default = " udp " , type = click . Choice ( [ ' udp ' , ' tcp ' ] ) , help = " OpenVPN transport protocol, UDP by default " )
2015-08-13 08:11:08 +00:00
@click.option ( " --route " , " -r " , type = ip_network , multiple = True , help = " Subnets to advertise via this connection, multiple allowed " )
2015-07-26 20:34:46 +00:00
@click.option ( " --config " , " -o " ,
default = " /etc/openvpn/site-to-client.conf " ,
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
help = " OpenVPN configuration file " )
2017-05-04 06:40:47 +00:00
@fqdn_required
2017-05-03 21:12:51 +00:00
@setup_client ( prefix = " server_ " , dh = True )
2017-05-03 21:03:51 +00:00
def certidude_setup_openvpn_server ( authority , common_name , config , subnet , route , local , proto , port , * * paths ) :
2017-04-20 05:20:10 +00:00
# Install dependencies
apt ( " openvpn " )
rpm ( " openvpn " )
2015-07-26 20:34:46 +00:00
2016-09-17 21:00:14 +00:00
# Create corresponding section in /etc/certidude/services.conf
2017-05-01 16:20:50 +00:00
endpoint = " OpenVPN server %s of %s " % ( common_name , authority )
2016-09-17 21:00:14 +00:00
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , not reconfiguring " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
else :
service_config . add_section ( endpoint )
service_config . set ( endpoint , " authority " , authority )
service_config . set ( endpoint , " service " , " init/openvpn " )
2017-03-13 11:42:58 +00:00
2017-12-30 13:57:48 +00:00
with open ( const . SERVICES_CONFIG_PATH + " .part " , ' w ' ) as fh :
2016-09-17 21:00:14 +00:00
service_config . write ( fh )
os . rename ( const . SERVICES_CONFIG_PATH + " .part " , const . SERVICES_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2017-03-13 11:42:58 +00:00
authority_hostname = authority . split ( " . " ) [ 0 ]
config . write ( " server %s %s \n " % ( subnet . network_address , subnet . netmask ) )
config . write ( " dev tun- %s \n " % authority_hostname )
2016-09-17 21:00:14 +00:00
config . write ( " proto %s \n " % proto )
config . write ( " port %d \n " % port )
config . write ( " local %s \n " % local )
2017-05-03 21:03:51 +00:00
config . write ( " key %s \n " % paths . get ( " key_path " ) )
config . write ( " cert %s \n " % paths . get ( " certificate_path " ) )
config . write ( " ca %s \n " % paths . get ( " authority_path " ) )
config . write ( " crl-verify %s \n " % paths . get ( " revocations_path " ) )
config . write ( " dh %s \n " % paths . get ( " dhparam_path " ) )
2016-09-17 21:00:14 +00:00
config . write ( " comp-lzo \n " )
config . write ( " user nobody \n " )
config . write ( " group nogroup \n " )
config . write ( " persist-tun \n " )
config . write ( " persist-key \n " )
2017-03-13 11:42:58 +00:00
config . write ( " #ifconfig-pool-persist /tmp/openvpn-leases.txt \n " )
2017-05-03 21:03:51 +00:00
2015-07-26 20:34:46 +00:00
click . echo ( " Generated %s " % config . name )
2016-09-17 21:00:14 +00:00
click . echo ( " Inspect generated files and issue following to request certificate: " )
2015-07-26 20:34:46 +00:00
click . echo ( )
2017-12-30 13:57:48 +00:00
click . echo ( " certidude enroll " )
2015-07-26 20:34:46 +00:00
2016-03-21 21:42:39 +00:00
@click.command ( " nginx " , help = " Set up nginx as HTTPS server " )
2017-03-13 11:42:58 +00:00
@click.argument ( " authority " )
2016-09-17 21:00:14 +00:00
@click.option ( " --common-name " , " -cn " , default = const . FQDN , help = " Common name, %s by default " % const . FQDN )
2016-03-21 21:42:39 +00:00
@click.option ( " --tls-config " ,
default = " /etc/nginx/conf.d/tls.conf " ,
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
help = " TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default " )
@click.option ( " --site-config " , " -o " ,
2016-09-17 21:00:14 +00:00
default = " /etc/nginx/sites-available/ %s .conf " % const . HOSTNAME ,
2016-03-21 21:42:39 +00:00
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
2016-09-17 21:00:14 +00:00
help = " Site configuration file of nginx, /etc/nginx/sites-available/ %s .conf by default " % const . HOSTNAME )
2017-03-13 11:42:58 +00:00
@click.option ( " --verify-client " , " -vc " , default = " optional " , type = click . Choice ( [ ' optional ' , ' on ' , ' off ' ] ) )
2017-05-04 06:40:47 +00:00
@fqdn_required
2017-05-03 21:12:51 +00:00
@setup_client ( prefix = " server_ " , dh = True )
2017-05-03 21:03:51 +00:00
def certidude_setup_nginx ( authority , common_name , site_config , tls_config , verify_client , * * paths ) :
2017-05-04 06:40:47 +00:00
2017-05-03 21:03:51 +00:00
apt ( " nginx " )
rpm ( " nginx " )
from jinja2 import Environment , PackageLoader
env = Environment ( loader = PackageLoader ( " certidude " , " templates " ) , trim_blocks = True )
2016-03-21 21:42:39 +00:00
2016-09-17 21:00:14 +00:00
context = globals ( ) # Grab const.BLAH
2016-03-21 21:42:39 +00:00
context . update ( locals ( ) )
2017-05-03 21:03:51 +00:00
context . update ( paths )
2016-03-21 21:42:39 +00:00
2017-03-13 11:42:58 +00:00
if os . path . exists ( site_config . name ) :
click . echo ( " Configuration file %s already exists, not overwriting " % site_config . name )
2016-03-21 21:42:39 +00:00
else :
2017-03-13 11:42:58 +00:00
site_config . write ( env . get_template ( " nginx-https-site.conf " ) . render ( context ) )
click . echo ( " Generated %s " % site_config . name )
2016-03-21 21:42:39 +00:00
2017-03-13 11:42:58 +00:00
if os . path . exists ( tls_config . name ) :
click . echo ( " Configuration file %s already exists, not overwriting " % tls_config . name )
2016-03-21 21:42:39 +00:00
else :
2017-03-13 11:42:58 +00:00
tls_config . write ( env . get_template ( " nginx-tls.conf " ) . render ( context ) )
click . echo ( " Generated %s " % tls_config . name )
2016-03-21 21:42:39 +00:00
click . echo ( )
click . echo ( " Inspect configuration files, enable it and start nginx service: " )
click . echo ( )
click . echo ( " ln -s %s /etc/nginx/sites-enabled/ %s " % (
2017-03-13 11:42:58 +00:00
os . path . relpath ( site_config . name , " /etc/nginx/sites-enabled " ) ,
os . path . basename ( site_config . name ) ) )
click . echo ( " service nginx restart " )
2016-03-21 21:42:39 +00:00
click . echo ( )
2015-07-26 20:34:46 +00:00
@click.command ( " client " , help = " Set up OpenVPN client " )
2016-09-17 21:00:14 +00:00
@click.argument ( " authority " )
2015-07-26 20:34:46 +00:00
@click.argument ( " remote " )
2017-05-01 16:20:50 +00:00
@click.option ( " --common-name " , " -cn " , default = const . HOSTNAME , help = " Common name, %s by default " % const . HOSTNAME )
2015-07-26 20:34:46 +00:00
@click.option ( ' --proto ' , " -t " , default = " udp " , type = click . Choice ( [ ' udp ' , ' tcp ' ] ) , help = " OpenVPN transport protocol, UDP by default " )
@click.option ( " --config " , " -o " ,
2017-05-06 21:07:41 +00:00
default = " /etc/openvpn/client-to-site.conf " , # TODO: created initially disabled conf
2015-07-26 20:34:46 +00:00
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
help = " OpenVPN configuration file " )
2017-05-03 21:03:51 +00:00
@setup_client ( )
2017-05-04 06:40:47 +00:00
def certidude_setup_openvpn_client ( authority , remote , common_name , config , proto , * * paths ) :
2017-04-20 05:20:10 +00:00
# Install dependencies
apt ( " openvpn " )
rpm ( " openvpn " )
2016-09-17 21:00:14 +00:00
# Create corresponding section in /etc/certidude/services.conf
2017-04-20 05:20:10 +00:00
endpoint = " OpenVPN to %s " % remote
2016-09-17 21:00:14 +00:00
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , not reconfiguring " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
else :
service_config . add_section ( endpoint )
service_config . set ( endpoint , " authority " , authority )
service_config . set ( endpoint , " service " , " init/openvpn " )
2017-04-13 23:49:11 +00:00
service_config . set ( endpoint , " remote " , remote )
2017-12-30 13:57:48 +00:00
with open ( const . SERVICES_CONFIG_PATH + " .part " , ' w ' ) as fh :
2016-09-17 21:00:14 +00:00
service_config . write ( fh )
os . rename ( const . SERVICES_CONFIG_PATH + " .part " , const . SERVICES_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
config . write ( " client \n " )
config . write ( " remote %s \n " % remote )
config . write ( " remote-cert-tls server \n " )
config . write ( " proto %s \n " % proto )
2017-05-03 21:03:51 +00:00
config . write ( " dev tun- %s \n " % remote . split ( " . " ) [ 0 ] )
2016-09-17 21:00:14 +00:00
config . write ( " nobind \n " )
2017-05-04 17:56:53 +00:00
config . write ( " key %s \n " % paths . get ( " key_path " ) )
config . write ( " cert %s \n " % paths . get ( " certificate_path " ) )
config . write ( " ca %s \n " % paths . get ( " authority_path " ) )
config . write ( " crl-verify %s \n " % paths . get ( " revocations_path " ) )
2016-09-17 21:00:14 +00:00
config . write ( " comp-lzo \n " )
config . write ( " user nobody \n " )
config . write ( " group nogroup \n " )
config . write ( " persist-tun \n " )
config . write ( " persist-key \n " )
2017-05-01 16:20:50 +00:00
config . write ( " up /etc/openvpn/update-resolv-conf \n " )
config . write ( " down /etc/openvpn/update-resolv-conf \n " )
2015-07-26 20:34:46 +00:00
click . echo ( " Generated %s " % config . name )
2016-09-17 21:00:14 +00:00
click . echo ( " Inspect generated files and issue following to request certificate: " )
2015-07-26 20:34:46 +00:00
click . echo ( )
2017-12-30 13:57:48 +00:00
click . echo ( " certidude enroll " )
2015-07-26 20:34:46 +00:00
2015-08-13 08:11:08 +00:00
@click.command ( " server " , help = " Set up strongSwan server " )
2017-03-13 11:42:58 +00:00
@click.argument ( " authority " )
2017-04-14 17:21:31 +00:00
@click.option ( " --common-name " , " -cn " , default = const . FQDN , help = " Common name, %s by default " % const . FQDN )
2017-12-30 13:57:48 +00:00
@click.option ( " --subnet " , " -sn " , default = " 192.168.33.0/24 " , type = ip_network , help = " IPsec virtual subnet, 192.168.33.0/24 by default " )
2015-08-13 08:11:08 +00:00
@click.option ( " --route " , " -r " , type = ip_network , multiple = True , help = " Subnets to advertise via this connection, multiple allowed " )
2017-05-04 06:40:47 +00:00
@fqdn_required
2017-05-03 21:03:51 +00:00
@setup_client ( prefix = " server_ " )
def certidude_setup_strongswan_server ( authority , common_name , subnet , route , * * paths ) :
2017-04-20 05:20:10 +00:00
# Install dependencies
apt ( " strongswan " )
rpm ( " strongswan " )
2017-05-03 21:03:51 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = " IPsec gateway for %s " % authority
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , not reconfiguring " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2016-09-17 21:00:14 +00:00
else :
2017-05-03 21:03:51 +00:00
service_config . add_section ( endpoint )
service_config . set ( endpoint , " authority " , authority )
service_config . set ( endpoint , " service " , " init/strongswan " )
2017-12-30 13:57:48 +00:00
with open ( const . SERVICES_CONFIG_PATH + " .part " , ' w ' ) as fh :
2017-05-03 21:03:51 +00:00
service_config . write ( fh )
os . rename ( const . SERVICES_CONFIG_PATH + " .part " , const . SERVICES_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2016-09-17 21:00:14 +00:00
# Create corresponding section to /etc/ipsec.conf
from ipsecparse import loads
2017-05-06 21:07:41 +00:00
ipsec_conf = loads ( open ( " %s /ipsec.conf " % const . STRONGSWAN_PREFIX ) . read ( ) )
ipsec_conf [ " ca " , authority ] = dict (
auto = " add " ,
cacert = paths . get ( " authority_path " ) )
ipsec_conf [ " conn " , authority ] = dict (
2017-05-03 21:03:51 +00:00
leftcert = paths . get ( " certificate_path " ) ,
2017-04-14 17:21:31 +00:00
leftsubnet = " , " . join ( route ) ,
2016-09-17 21:00:14 +00:00
right = " %a ny " ,
2017-04-14 17:21:31 +00:00
rightsourceip = str ( subnet ) ,
2016-09-17 21:00:14 +00:00
closeaction = " restart " ,
auto = " ignore " )
2017-05-06 21:07:41 +00:00
with open ( " %s /ipsec.conf " % const . STRONGSWAN_PREFIX , " w " ) as fh :
fh . write ( ipsec_conf . dumps ( ) )
with open ( " %s /ipsec.secrets " % const . STRONGSWAN_PREFIX , " a " ) as fh :
fh . write ( " : RSA %s \n " % paths . get ( " key_path " ) )
2015-08-13 08:11:08 +00:00
click . echo ( )
2016-03-29 15:37:28 +00:00
click . echo ( " If you ' re running Ubuntu make sure you ' re not affected by #1505222 " )
click . echo ( " https://bugs.launchpad.net/ubuntu/+source/strongswan/+bug/1505222 " )
2015-08-13 08:11:08 +00:00
@click.command ( " client " , help = " Set up strongSwan client " )
2017-03-13 11:42:58 +00:00
@click.argument ( " authority " )
2015-08-13 08:11:08 +00:00
@click.argument ( " remote " )
2017-05-03 14:42:37 +00:00
@click.option ( " --common-name " , " -cn " , default = const . HOSTNAME , help = " Common name, %s by default " % const . HOSTNAME )
2017-05-03 21:03:51 +00:00
@setup_client ( )
def certidude_setup_strongswan_client ( authority , remote , common_name , * * paths ) :
2017-04-20 05:20:10 +00:00
# Install dependencies
2017-05-06 21:07:41 +00:00
apt ( " strongswan " ) or rpm ( " strongswan " )
2017-04-20 05:20:10 +00:00
2017-04-13 23:49:11 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = " IPsec connection to %s " % remote
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , not reconfiguring " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
else :
service_config . add_section ( endpoint )
service_config . set ( endpoint , " authority " , authority )
service_config . set ( endpoint , " service " , " init/strongswan " )
service_config . set ( endpoint , " remote " , remote )
2017-12-30 13:57:48 +00:00
with open ( const . SERVICES_CONFIG_PATH + " .part " , ' w ' ) as fh :
2017-04-13 23:49:11 +00:00
service_config . write ( fh )
os . rename ( const . SERVICES_CONFIG_PATH + " .part " , const . SERVICES_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2016-09-17 21:00:14 +00:00
# Create corresponding section in /etc/ipsec.conf
from ipsecparse import loads
2017-05-06 21:07:41 +00:00
ipsec_conf = loads ( open ( ' %s /ipsec.conf ' % const . STRONGSWAN_PREFIX ) . read ( ) )
ipsec_conf [ " ca " , authority ] = dict (
auto = " add " ,
cacert = paths . get ( " authority_path " ) )
ipsec_conf [ " conn " , remote ] = dict (
2016-09-17 21:00:14 +00:00
leftsourceip = " %c onfig " ,
left = " %d efaultroute " ,
2017-05-03 21:03:51 +00:00
leftcert = paths . get ( " certificate_path " ) ,
2016-09-17 21:00:14 +00:00
rightid = " %a ny " ,
right = remote ,
2017-05-06 21:07:41 +00:00
rightsubnet = " 0.0.0.0/0 " , # To allow anything suggested by gateway
2016-09-17 21:00:14 +00:00
keyexchange = " ikev2 " ,
keyingtries = " 300 " ,
dpdaction = " restart " ,
closeaction = " restart " ,
auto = " ignore " )
2017-05-06 21:07:41 +00:00
with open ( " %s /ipsec.conf " % const . STRONGSWAN_PREFIX , " w " ) as fh :
fh . write ( ipsec_conf . dumps ( ) )
with open ( " %s /ipsec.secrets " % const . STRONGSWAN_PREFIX , " a " ) as fh :
fh . write ( " : RSA %s \n " % paths . get ( " key_path " ) )
if os . path . exists ( " /etc/apparmor.d/local " ) :
with open ( " /etc/apparmor.d/local/usr.lib.ipsec.charon " , " w " ) as fh :
fh . write ( os . path . join ( const . STORAGE_PATH , " ** " ) + " r, \n " )
2016-09-17 21:00:14 +00:00
2017-04-13 23:49:11 +00:00
click . echo ( " Generated section %s in %s " % ( authority , const . CLIENT_CONFIG_PATH ) )
2017-12-30 13:57:48 +00:00
click . echo ( " Run ' certidude enroll ' to request certificates and to enable services " )
2015-08-13 08:11:08 +00:00
2015-10-17 15:07:26 +00:00
@click.command ( " networkmanager " , help = " Set up strongSwan client via NetworkManager " )
2017-03-13 11:42:58 +00:00
@click.argument ( " authority " ) # Certidude server
2016-03-27 20:38:14 +00:00
@click.argument ( " remote " ) # StrongSwan gateway
2017-05-03 14:42:37 +00:00
@click.option ( " --common-name " , " -cn " , default = const . HOSTNAME , help = " Common name, %s by default " % const . HOSTNAME )
2017-05-03 21:03:51 +00:00
@setup_client ( )
def certidude_setup_strongswan_networkmanager ( authority , remote , common_name , * * paths ) :
2017-04-20 05:20:10 +00:00
# Install dependencies
2017-05-04 06:55:26 +00:00
apt ( " network-manager strongswan-nm " )
rpm ( " NetworkManager NetworkManager-tui NetworkManager-strongswan-gnome " )
2015-10-17 15:07:26 +00:00
2016-09-17 21:00:14 +00:00
# Create corresponding section in /etc/certidude/services.conf
2017-05-04 06:55:26 +00:00
endpoint = " IPSec to %s " % remote
2016-09-17 21:00:14 +00:00
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , remove to regenerate " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
else :
service_config . add_section ( endpoint )
2017-04-13 23:49:11 +00:00
service_config . set ( endpoint , " authority " , authority )
service_config . set ( endpoint , " remote " , remote )
service_config . set ( endpoint , " service " , " network-manager/strongswan " )
2017-12-30 13:57:48 +00:00
with open ( const . SERVICES_CONFIG_PATH + " .part " , ' w ' ) as fh :
2016-09-17 21:00:14 +00:00
service_config . write ( fh )
os . rename ( const . SERVICES_CONFIG_PATH + " .part " , const . SERVICES_CONFIG_PATH )
click . echo ( " Section ' %s ' added to %s " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2015-10-17 15:07:26 +00:00
2016-03-27 20:38:14 +00:00
@click.command ( " networkmanager " , help = " Set up OpenVPN client via NetworkManager " )
2017-03-13 11:42:58 +00:00
@click.argument ( " authority " )
2016-03-27 20:38:14 +00:00
@click.argument ( " remote " ) # OpenVPN gateway
2016-09-17 21:00:14 +00:00
@click.option ( " --common-name " , " -cn " , default = const . HOSTNAME , help = " Common name, %s by default " % const . HOSTNAME )
2017-05-03 21:03:51 +00:00
@setup_client ( )
def certidude_setup_openvpn_networkmanager ( authority , remote , common_name , * * paths ) :
2017-05-04 06:55:26 +00:00
apt ( " network-manager network-manager-openvpn-gnome " )
rpm ( " NetworkManager NetworkManager-tui NetworkManager-openvpn-gnome " )
2017-05-03 21:03:51 +00:00
# Create corresponding section in /etc/certidude/services.conf
2016-03-27 20:38:14 +00:00
endpoint = " OpenVPN to %s " % remote
2015-10-17 15:07:26 +00:00
2016-09-17 21:00:14 +00:00
service_config = ConfigParser ( )
if os . path . exists ( const . SERVICES_CONFIG_PATH ) :
service_config . readfp ( open ( const . SERVICES_CONFIG_PATH ) )
if service_config . has_section ( endpoint ) :
click . echo ( " Section ' %s ' already exists in %s , remove to regenerate " % ( endpoint , const . SERVICES_CONFIG_PATH ) )
2016-03-27 20:38:14 +00:00
else :
2016-09-17 21:00:14 +00:00
service_config . add_section ( endpoint )
2017-05-03 21:03:51 +00:00
service_config . set ( endpoint , " authority " , authority )
2016-09-17 21:00:14 +00:00
service_config . set ( endpoint , " remote " , remote )
service_config . set ( endpoint , " service " , " network-manager/openvpn " )
service_config . write ( open ( " /etc/certidude/services.conf " , " w " ) )
2016-03-27 20:38:14 +00:00
click . echo ( " Section %s added to /etc/certidude/client.conf " % endpoint )
2015-10-17 15:07:26 +00:00
2016-03-29 19:03:27 +00:00
@click.command ( " authority " , help = " Set up Certificate Authority in a directory " )
2015-08-13 08:11:08 +00:00
@click.option ( " --username " , default = " certidude " , help = " Service user account, created if necessary, ' certidude ' by default " )
2016-03-29 19:03:27 +00:00
@click.option ( " --kerberos-keytab " , default = " /etc/certidude/server.keytab " , help = " Kerberos keytab for using ' kerberos ' authentication backend, /etc/certidude/server.keytab by default " )
2015-08-13 08:11:08 +00:00
@click.option ( " --nginx-config " , " -n " ,
2016-03-29 19:03:27 +00:00
default = " /etc/nginx/sites-available/certidude.conf " ,
2015-08-13 08:11:08 +00:00
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
2016-03-29 19:03:27 +00:00
help = " nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default " )
2018-05-15 07:45:29 +00:00
@click.option ( " --tls-config " ,
default = " /etc/nginx/conf.d/tls.conf " ,
type = click . File ( mode = " w " , atomic = True , lazy = True ) ,
help = " TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default " )
2017-12-30 13:57:48 +00:00
@click.option ( " --common-name " , " -cn " , default = const . FQDN , help = " Common name of the server, %s by default " % const . FQDN )
@click.option ( " --title " , " -t " , default = " Certidude at %s " % const . FQDN , help = " Common name of the certificate authority, ' Certidude at %s ' by default " % const . FQDN )
2017-03-13 11:42:58 +00:00
@click.option ( " --authority-lifetime " , default = 20 * 365 , help = " Authority certificate lifetime in days, 20 years by default " )
2016-03-29 19:03:27 +00:00
@click.option ( " --organization " , " -o " , default = None , help = " Company or organization name " )
2018-04-27 07:48:15 +00:00
@click.option ( " --organizational-unit " , " -ou " , default = " Certificate Authority " )
2017-04-13 20:30:28 +00:00
@click.option ( " --push-server " , help = " Push server, by default http:// %s " % const . FQDN )
2018-05-07 11:18:29 +00:00
@click.option ( " --directory " , default = " /var/lib/certidude " , help = " Directory for authority files " )
2016-09-17 21:00:14 +00:00
@click.option ( " --outbox " , default = " smtp://smtp. %s " % const . DOMAIN , help = " SMTP server, smtp://smtp. %s by default " % const . DOMAIN )
2018-05-07 11:18:29 +00:00
@click.option ( " --skip-assets " , is_flag = True , help = " Don ' t attempt to assemble JS/CSS/font assets " )
2018-01-03 22:12:02 +00:00
@click.option ( " --skip-packages " , is_flag = True , help = " Don ' t attempt to install apt/pip/npm packages " )
2018-05-17 09:00:13 +00:00
@click.option ( " --packages-only " , is_flag = True , help = " Install only apt/pip/npm packages " )
2018-04-09 13:08:12 +00:00
@click.option ( " --elliptic-curve " , " -e " , is_flag = True , help = " Generate EC instead of RSA keypair " )
2018-05-07 11:18:29 +00:00
@click.option ( " --subordinate " , is_flag = True , help = " Set up subordinate CA instead of root CA " )
2018-05-17 09:00:13 +00:00
def certidude_setup_authority ( username , kerberos_keytab , nginx_config , tls_config , organization , organizational_unit , common_name , directory , authority_lifetime , push_server , outbox , title , skip_assets , skip_packages , elliptic_curve , subordinate , packages_only ) :
2018-05-02 08:11:01 +00:00
assert subprocess . check_output ( [ " /usr/bin/lsb_release " , " -cs " ] ) in ( b " trusty \n " , b " xenial \n " , b " bionic \n " ) , " Only Ubuntu 16.04 supported at the moment "
2017-12-30 13:57:48 +00:00
assert os . getuid ( ) == 0 and os . getgid ( ) == 0 , " Authority can be set up only by root "
import pwd
from jinja2 import Environment , PackageLoader
env = Environment ( loader = PackageLoader ( " certidude " , " templates " ) , trim_blocks = True )
2018-01-03 22:12:02 +00:00
if skip_packages :
2018-05-17 09:40:36 +00:00
click . echo ( " Not attempting to install packages as requested... " )
2017-12-30 13:57:48 +00:00
else :
2018-01-03 22:12:02 +00:00
click . echo ( " Installing packages... " )
2018-05-04 08:54:55 +00:00
cmd = " DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
cython3 python3 - dev \
2018-01-03 22:12:02 +00:00
python3 - markdown python3 - pyxattr python3 - jinja2 python3 - cffi \
software - properties - common libsasl2 - modules - gssapi - mit npm nodejs \
2018-04-27 07:48:15 +00:00
libkrb5 - dev libldap2 - dev libsasl2 - dev gawk libncurses5 - dev \
2018-05-04 08:54:55 +00:00
rsync attr wget unzip "
click . echo ( " Running: %s " % cmd )
2018-05-15 07:45:29 +00:00
if os . system ( cmd ) :
raise click . ClickException ( " Failed to install APT packages " )
if os . system ( " pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents " ) :
raise click . ClickException ( " Failed to install Python packages " )
if os . system ( " pip3 install -q --pre --upgrade python-ldap " ) :
raise click . ClickException ( " Failed to install python-ldap " )
2018-01-03 22:12:02 +00:00
if not os . path . exists ( " /usr/lib/nginx/modules/ngx_nchan_module.so " ) :
click . echo ( " Enabling nginx PPA " )
2018-05-15 07:45:29 +00:00
if os . system ( " add-apt-repository -y ppa:nginx/stable " ) :
raise click . ClickException ( " Failed to add nginx PPA " )
if os . system ( " apt-get update -q " ) :
raise click . ClickException ( " Failed to update package lists " )
if os . system ( " apt-get install -y -q libnginx-mod-nchan " ) :
raise click . ClickException ( " Failed to install nchan " )
2018-01-03 22:12:02 +00:00
else :
click . echo ( " PPA for nginx already enabled " )
2017-12-30 13:57:48 +00:00
2018-01-03 22:12:02 +00:00
if not os . path . exists ( " /usr/sbin/nginx " ) :
click . echo ( " Installing nginx from PPA " )
2018-05-15 07:45:29 +00:00
if os . system ( " apt-get install -y -q nginx " ) :
raise click . ClickException ( " Failed to install nginx " )
2018-01-03 22:12:02 +00:00
else :
click . echo ( " Web server nginx already installed " )
2017-05-06 21:35:02 +00:00
2018-05-17 09:40:36 +00:00
cmd = " npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg "
click . echo ( " Installing JavaScript packages: %s " % cmd )
if os . system ( cmd ) :
raise click . ClickException ( " Failed to install JavaScript packages " )
2018-05-17 09:00:13 +00:00
if not os . path . exists ( " /usr/bin/node " ) :
os . symlink ( " /usr/bin/nodejs " , " /usr/bin/node " )
if packages_only :
return
2017-04-20 05:20:10 +00:00
2018-05-17 09:15:52 +00:00
if " . " in common_name :
logger . info ( " Using fully qualified hostname %s " % common_name )
else :
raise ValueError ( " Fully qualified hostname not specified as common name, make sure hostname -f works " )
2017-04-22 11:10:54 +00:00
# Generate secret for tokens
2018-05-17 09:15:52 +00:00
token_url = " https:// " + common_name + " /#action=enroll&token= %(token)s &router= %(router)s &protocol=ovpn "
2017-04-22 11:10:54 +00:00
2017-12-30 13:57:48 +00:00
template_path = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , " templates " , " profile " )
2017-05-01 18:06:47 +00:00
click . echo ( " Using templates from %s " % template_path )
2016-09-17 21:00:14 +00:00
2017-05-01 18:06:47 +00:00
click . echo ( " Placing authority files in %s " % directory )
2016-09-17 21:00:14 +00:00
2017-03-13 11:42:58 +00:00
certificate_url = " http:// %s /api/certificate/ " % common_name
2017-05-01 18:06:47 +00:00
click . echo ( " Setting CA certificate URL to %s " % certificate_url )
2017-03-13 11:42:58 +00:00
revoked_url = " http:// %s /api/revoked/ " % common_name
2017-05-01 18:06:47 +00:00
click . echo ( " Setting revocation list URL to %s " % revoked_url )
2016-03-29 19:03:27 +00:00
2018-05-15 07:45:29 +00:00
responder_url = " http:// %s /api/ocsp/ " % common_name
click . echo ( " Setting OCSP responder URL to %s " % responder_url )
2016-03-29 19:03:27 +00:00
# Expand variables
2017-12-30 13:57:48 +00:00
assets_dir = os . path . join ( directory , " assets " )
2016-03-29 19:03:27 +00:00
ca_key = os . path . join ( directory , " ca_key.pem " )
2018-05-07 11:18:29 +00:00
ca_req = os . path . join ( directory , " ca_req.pem " )
2017-12-30 13:57:48 +00:00
ca_cert = os . path . join ( directory , " ca_cert.pem " )
2018-05-07 11:18:29 +00:00
self_key = os . path . join ( directory , " self_key.pem " )
2017-05-25 19:20:29 +00:00
sqlite_path = os . path . join ( directory , " meta " , " db.sqlite " )
2018-05-17 09:15:52 +00:00
distinguished_name = cn_to_dn ( title , common_name , o = organization , ou = organizational_unit )
2018-05-15 07:45:29 +00:00
dhparam_path = " /etc/ssl/dhparam.pem "
2016-09-17 21:00:14 +00:00
2018-05-02 08:11:01 +00:00
# Builder variables
dhgroup = " ecp384 " if elliptic_curve else " modp2048 "
2017-05-03 07:04:52 +00:00
try :
pwd . getpwnam ( " certidude " )
click . echo ( " User ' certidude ' already exists " )
except KeyError :
cmd = " adduser " , " --system " , " --no-create-home " , " --group " , " certidude "
if subprocess . call ( cmd ) :
2018-05-15 07:45:29 +00:00
raise click . ClickException ( " Failed to create system user ' certidude ' " )
2017-05-03 07:04:52 +00:00
if os . path . exists ( kerberos_keytab ) :
click . echo ( " Service principal keytab found in ' %s ' " % kerberos_keytab )
else :
2018-05-17 20:18:19 +00:00
click . echo ( " To use ' kerberos ' authentication backend join the domain , create service principal and provision authority again: " )
2017-05-03 07:04:52 +00:00
click . echo ( )
2018-05-17 20:18:19 +00:00
click . echo ( " kinit administrator@EXAMPLE.LAN " )
click . echo ( " net ads join -k " )
2017-05-03 07:04:52 +00:00
click . echo ( " KRB5_KTNAME=FILE: %s net ads keytab add HTTP -P " % kerberos_keytab )
2018-05-17 20:18:19 +00:00
click . echo ( " kdestroy " )
2017-05-03 07:04:52 +00:00
click . echo ( " chown %s %s " % ( username , kerberos_keytab ) )
2018-05-17 20:18:19 +00:00
click . echo ( " mv /etc/certidude/server.conf /etc/certidude/server.backup " )
click . echo ( " certidude setup authority " )
2017-05-03 07:04:52 +00:00
click . echo ( )
2015-08-13 08:11:08 +00:00
2017-12-30 13:57:48 +00:00
2017-05-03 07:04:52 +00:00
if os . path . exists ( " /etc/krb5.keytab " ) and os . path . exists ( " /etc/samba/smb.conf " ) :
# Fetch Kerberos ticket for system account
cp = ConfigParser ( )
cp . read ( " /etc/samba/smb.conf " )
realm = cp . get ( " global " , " realm " )
domain = realm . lower ( )
name = cp . get ( " global " , " netbios name " )
base = " , " . join ( [ " dc= " + j for j in domain . split ( " . " ) ] )
if not os . path . exists ( " /etc/cron.hourly/certidude " ) :
with open ( " /etc/cron.hourly/certidude " , " w " ) as fh :
fh . write ( env . get_template ( " server/cronjob " ) . render ( vars ( ) ) )
os . chmod ( " /etc/cron.hourly/certidude " , 0o755 )
click . echo ( " Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly " )
os . system ( " /etc/cron.hourly/certidude " )
else :
click . echo ( " Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured " )
2015-08-13 08:11:08 +00:00
2018-05-02 08:11:01 +00:00
letsencrypt_fullchain = " /etc/letsencrypt/live/ %s /fullchain.pem " % common_name
letsencrypt_privkey = " /etc/letsencrypt/live/ %s /privkey.pem " % common_name
letsencrypt = os . path . exists ( letsencrypt_fullchain )
2018-05-15 07:45:29 +00:00
2018-05-17 20:18:19 +00:00
builder_path = os . path . join ( os . path . realpath ( os . path . dirname ( __file__ ) ) , " builder " )
2018-01-03 22:12:02 +00:00
script_dir = os . path . join ( os . path . realpath ( os . path . dirname ( __file__ ) ) , " templates " , " script " )
2017-05-03 07:04:52 +00:00
static_path = os . path . join ( os . path . realpath ( os . path . dirname ( __file__ ) ) , " static " )
certidude_path = sys . argv [ 0 ]
2015-08-13 08:11:08 +00:00
2017-07-05 15:22:03 +00:00
click . echo ( " Generating: %s " % nginx_config . name )
nginx_config . write ( env . get_template ( " server/nginx.conf " ) . render ( vars ( ) ) )
nginx_config . close ( )
if not os . path . exists ( " /etc/nginx/sites-enabled/certidude.conf " ) :
os . symlink ( " ../sites-available/certidude.conf " , " /etc/nginx/sites-enabled/certidude.conf " )
click . echo ( " Symlinked %s -> /etc/nginx/sites-enabled/ " % nginx_config . name )
if os . path . exists ( " /etc/nginx/sites-enabled/default " ) :
os . unlink ( " /etc/nginx/sites-enabled/default " )
2017-05-03 07:04:52 +00:00
if os . path . exists ( " /etc/systemd " ) :
if os . path . exists ( " /etc/systemd/system/certidude.service " ) :
click . echo ( " File /etc/systemd/system/certidude.service already exists, remove to regenerate " )
2016-09-17 21:00:14 +00:00
else :
2017-05-03 07:04:52 +00:00
with open ( " /etc/systemd/system/certidude.service " , " w " ) as fh :
fh . write ( env . get_template ( " server/systemd.service " ) . render ( vars ( ) ) )
click . echo ( " File /etc/systemd/system/certidude.service created " )
2018-05-17 09:00:13 +00:00
os . system ( " systemctl daemon-reload " )
2016-09-17 21:00:14 +00:00
else :
2018-05-17 09:00:13 +00:00
raise NotImplementedError ( " Not systemd based OS, don ' t know how to set up initscripts " )
2017-05-03 07:04:52 +00:00
2018-04-27 07:48:15 +00:00
# Set umask to 0022
os . umask ( 0o022 )
2017-12-30 13:57:48 +00:00
assert os . getuid ( ) == 0 and os . getgid ( ) == 0
2018-04-27 07:48:15 +00:00
2017-12-30 13:57:48 +00:00
bootstrap_pid = os . fork ( )
if not bootstrap_pid :
2018-04-27 07:48:15 +00:00
# Create what's usually /var/lib/certidude
if not os . path . exists ( directory ) :
os . makedirs ( directory )
assert os . stat ( directory ) . st_mode == 0o40755
2017-12-30 13:57:48 +00:00
# Create bundle directories
bundle_js = os . path . join ( assets_dir , " js " , " bundle.js " )
bundle_css = os . path . join ( assets_dir , " css " , " bundle.css " )
for path in bundle_js , bundle_css :
subdir = os . path . dirname ( path )
if not os . path . exists ( subdir ) :
2018-01-03 22:34:52 +00:00
click . echo ( " Creating directory %s " % subdir )
2017-12-30 13:57:48 +00:00
os . makedirs ( subdir )
2018-05-07 11:18:29 +00:00
if skip_assets :
click . echo ( " Not attempting to assemble assets as requested... " )
else :
# Copy fonts
click . echo ( " Copying fonts... " )
2018-05-15 07:45:29 +00:00
if os . system ( " rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s /fonts/ " % assets_dir ) :
raise click . ClickException ( " Failed to copy fonts " )
2018-05-07 11:18:29 +00:00
# Compile nunjucks templates
2018-05-15 07:45:29 +00:00
cmd = ' nunjucks-precompile --include " \ .html$ " --include " \ .ps1$ " --include " \ .sh$ " --include " \ .svg$ " --include " \ .yml$ " --include " \ .conf$ " --include " \ .mobileconfig$ " %s > %s .part ' % ( static_path , bundle_js )
2018-05-07 11:18:29 +00:00
click . echo ( " Compiling templates: %s " % cmd )
2018-05-15 07:45:29 +00:00
if os . system ( cmd ) :
raise click . ClickException ( " Failed to compile nunjucks templates " )
2018-05-07 11:18:29 +00:00
# Assemble bundle.js
click . echo ( " Assembling %s " % bundle_js )
with open ( bundle_js + " .part " , " a " ) as fh :
2018-05-15 07:45:29 +00:00
for pkg in " jquery/dist/jquery.min.js " , " tether/dist/js/*.min.js " , " bootstrap/dist/js/*.min.js " , " node-forge/dist/forge.all.min.js " , " qrcode-svg/dist/qrcode.min.js " , " timeago/*.js " , " nunjucks/browser/nunjucks-slim.min.js " :
2018-05-07 11:18:29 +00:00
for j in glob ( os . path . join ( " /usr/local/lib/node_modules " , pkg ) ) :
click . echo ( " - Merging: %s " % j )
with open ( j ) as ih :
fh . write ( ih . read ( ) )
# Assemble bundle.css
click . echo ( " Assembling %s " % bundle_css )
with open ( bundle_css + " .part " , " w " ) as fh :
for pkg in " tether/dist/css/*.min.css " , " bootstrap/dist/css/*.min.*css " , " font-awesome/css/font-awesome.min.css " :
for j in glob ( os . path . join ( " /usr/local/lib/node_modules " , pkg ) ) :
click . echo ( " - Merging: %s " % j )
with open ( j ) as ih :
fh . write ( ih . read ( ) )
os . rename ( bundle_css + " .part " , bundle_css )
os . rename ( bundle_js + " .part " , bundle_js )
2017-12-30 13:57:48 +00:00
assert os . getuid ( ) == 0 and os . getgid ( ) == 0
_ , _ , uid , gid , gecos , root , shell = pwd . getpwnam ( " certidude " )
os . setgid ( gid )
# Generate Certidude server config
if not os . path . exists ( const . CONFIG_DIR ) :
click . echo ( " Creating %s " % const . CONFIG_DIR )
os . makedirs ( const . CONFIG_DIR )
2018-05-15 07:45:29 +00:00
if not os . path . exists ( const . SCRIPT_DIR ) :
click . echo ( " Creating %s " % const . SCRIPT_DIR )
os . makedirs ( const . SCRIPT_DIR )
2018-04-27 07:48:15 +00:00
2018-05-07 11:18:29 +00:00
os . umask ( 0o177 ) # 600
2018-05-15 07:45:29 +00:00
if not os . path . exists ( dhparam_path ) :
2018-05-17 09:00:13 +00:00
cmd = " openssl " , " dhparam " , " -out " , dhparam_path , str ( const . KEY_SIZE )
2018-05-15 07:45:29 +00:00
subprocess . check_call ( cmd )
if os . path . exists ( tls_config . name ) :
click . echo ( " Configuration file %s already exists, not overwriting " % tls_config . name )
else :
tls_config . write ( env . get_template ( " nginx-tls.conf " ) . render ( locals ( ) ) )
click . echo ( " Generated %s " % tls_config . name )
2018-01-03 22:12:02 +00:00
if os . path . exists ( const . SERVER_CONFIG_PATH ) :
click . echo ( " Configuration file %s already exists, remove to regenerate " % const . SERVER_CONFIG_PATH )
2017-05-25 19:20:29 +00:00
else :
2017-12-30 13:57:48 +00:00
push_token = " " . join ( [ random . choice ( string . ascii_letters + string . digits ) for j in range ( 0 , 32 ) ] )
2018-01-03 22:12:02 +00:00
with open ( const . SERVER_CONFIG_PATH , " w " ) as fh :
2017-12-30 13:57:48 +00:00
fh . write ( env . get_template ( " server/server.conf " ) . render ( vars ( ) ) )
2018-01-03 22:12:02 +00:00
click . echo ( " Generated %s " % const . SERVER_CONFIG_PATH )
2018-01-03 22:34:52 +00:00
# Create image builder config
if os . path . exists ( const . BUILDER_CONFIG_PATH ) :
click . echo ( " Image builder config %s already exists, remove to regenerate " % const . BUILDER_CONFIG_PATH )
else :
with open ( const . BUILDER_CONFIG_PATH , " w " ) as fh :
fh . write ( env . get_template ( " server/builder.conf " ) . render ( vars ( ) ) )
click . echo ( " File %s created " % const . BUILDER_CONFIG_PATH )
2017-12-30 13:57:48 +00:00
2018-05-15 07:45:29 +00:00
# Create image builder site script
if os . path . exists ( const . BUILDER_SITE_SCRIPT ) :
click . echo ( " Image builder site customization script %s already exists, remove to regenerate " % const . BUILDER_SITE_SCRIPT )
else :
with open ( const . BUILDER_SITE_SCRIPT , " w " ) as fh :
fh . write ( env . get_template ( " server/site.sh " ) . render ( vars ( ) ) )
click . echo ( " File %s created " % const . BUILDER_SITE_SCRIPT )
2018-04-27 07:48:15 +00:00
# Create signature profile config
2018-04-16 12:13:31 +00:00
if os . path . exists ( const . PROFILE_CONFIG_PATH ) :
click . echo ( " Signature profile config %s already exists, remove to regenerate " % const . PROFILE_CONFIG_PATH )
else :
with open ( const . PROFILE_CONFIG_PATH , " w " ) as fh :
fh . write ( env . get_template ( " server/profile.conf " ) . render ( vars ( ) ) )
click . echo ( " File %s created " % const . PROFILE_CONFIG_PATH )
2017-12-30 13:57:48 +00:00
# Create subdirectories with 770 permissions
os . umask ( 0o007 )
2018-05-15 07:45:29 +00:00
for subdir in ( " signed " , " signed/by-serial " , " requests " , " revoked " , " expired " , " meta " , " builder " ) :
2017-12-30 13:57:48 +00:00
path = os . path . join ( directory , subdir )
if not os . path . exists ( path ) :
click . echo ( " Creating directory %s " % path )
os . mkdir ( path )
else :
click . echo ( " Directory already exists %s " % path )
2018-05-15 07:45:29 +00:00
assert os . stat ( path ) . st_mode == 0o40770 , path
2017-07-27 21:53:11 +00:00
2017-12-30 13:57:48 +00:00
# Create SQLite database file with correct permissions
2018-04-27 07:48:15 +00:00
os . umask ( 0o117 )
2017-12-30 13:57:48 +00:00
if not os . path . exists ( sqlite_path ) :
with open ( sqlite_path , " wb " ) as fh :
pass
2015-07-26 20:34:46 +00:00
2017-12-30 13:57:48 +00:00
# Generate and sign CA key
2018-05-07 11:18:29 +00:00
if not os . path . exists ( ca_key ) or subordinate and not os . path . exists ( ca_req ) :
2018-04-09 13:08:12 +00:00
if elliptic_curve :
click . echo ( " Generating %s EC key for CA ... " % const . CURVE_NAME )
public_key , private_key = asymmetric . generate_pair ( " ec " , curve = const . CURVE_NAME )
else :
click . echo ( " Generating %d -bit RSA key for CA ... " % const . KEY_SIZE )
public_key , private_key = asymmetric . generate_pair ( " rsa " , bit_size = const . KEY_SIZE )
2017-12-30 13:57:48 +00:00
2018-05-07 11:18:29 +00:00
# Set permission bits to 600
os . umask ( 0o177 )
with open ( ca_key , ' wb ' ) as f :
f . write ( asymmetric . dump_private_key ( private_key , None ) )
if subordinate :
builder = CSRBuilder ( distinguished_name , public_key )
request = builder . build ( private_key )
with open ( ca_req + " .part " , ' wb ' ) as f :
f . write ( pem_armor_csr ( request ) )
os . rename ( ca_req + " .part " , ca_req )
if not os . path . exists ( ca_cert ) :
if subordinate :
click . echo ( " Request has been written to %s " % ca_req )
click . echo ( )
click . echo ( open ( ca_req ) . read ( ) )
click . echo ( )
click . echo ( " Get it signed and insert signed certificate into %s " % ca_cert )
click . echo ( )
click . echo ( " cat > %s " % ca_cert )
click . echo ( )
click . echo ( " Paste contents and press Ctrl-D, adjust permissions: " )
click . echo ( )
click . echo ( " chown root:root %s " % ca_cert )
click . echo ( " chmod 0644 %s " % ca_cert )
click . echo ( )
click . echo ( " To finish setup procedure run ' certidude setup authority ' again " )
2018-05-15 07:45:29 +00:00
sys . exit ( 1 ) # stop this fork here with error
2018-05-07 11:18:29 +00:00
2018-04-27 07:48:15 +00:00
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
2018-05-07 11:18:29 +00:00
builder = CertificateBuilder ( distinguished_name , public_key )
2017-12-30 13:57:48 +00:00
builder . self_signed = True
builder . ca = True
2018-05-15 07:45:29 +00:00
builder . serial_number = generate_serial ( )
2017-12-30 13:57:48 +00:00
2018-05-15 07:45:29 +00:00
builder . begin_date = NOW - const . CLOCK_SKEW_TOLERANCE
2017-12-30 13:57:48 +00:00
builder . end_date = NOW + timedelta ( days = authority_lifetime )
certificate = builder . build ( private_key )
# Set permission bits to 640
os . umask ( 0o137 )
with open ( ca_cert , ' wb ' ) as f :
f . write ( pem_armor_certificate ( certificate ) )
2018-05-15 07:45:29 +00:00
click . echo ( " Authority certificate written to: %s " % ca_cert )
2017-12-30 13:57:48 +00:00
sys . exit ( 0 ) # stop this fork here
else :
2018-05-15 07:45:29 +00:00
2018-05-07 11:18:29 +00:00
_ , exitcode = os . waitpid ( bootstrap_pid , 0 )
if exitcode :
return 0
2017-12-30 13:57:48 +00:00
from certidude import authority
2018-05-02 08:11:01 +00:00
authority . self_enroll ( skip_notify = True )
2018-05-15 07:45:29 +00:00
assert os . path . exists ( self_key )
2018-05-17 09:15:52 +00:00
assert os . path . exists ( os . path . join ( directory , " signed " , common_name ) + " .pem " )
2017-12-30 13:57:48 +00:00
assert os . getuid ( ) == 0 and os . getgid ( ) == 0 , " Enroll contaminated environment "
2018-05-07 11:18:29 +00:00
assert os . stat ( sqlite_path ) . st_mode == 0o100660
assert os . stat ( ca_cert ) . st_mode == 0o100640
assert os . stat ( ca_key ) . st_mode == 0o100600
assert os . stat ( " /etc/nginx/sites-available/certidude.conf " ) . st_mode == 0o100600
assert os . stat ( " /etc/certidude/server.conf " ) . st_mode == 0o100600
2018-01-03 22:12:02 +00:00
click . echo ( " To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s " % const . SERVER_CONFIG_PATH )
2017-12-30 13:57:48 +00:00
click . echo ( )
click . echo ( " Use following commands to inspect the newly created files: " )
click . echo ( )
click . echo ( " openssl x509 -text -noout -in %s | less " % ca_cert )
click . echo ( " openssl rsa -check -in %s " % ca_key )
click . echo ( " openssl verify -CAfile %s %s " % ( ca_cert , ca_cert ) )
2018-05-15 07:45:29 +00:00
click . echo ( )
click . echo ( " To inspect logs and issued tokens: " )
click . echo ( )
click . echo ( " echo ' select * from log; ' | sqlite3 /var/lib/certidude/meta/db.sqlite " )
click . echo ( " echo ' select * from token; ' | sqlite3 /var/lib/certidude/meta/db.sqlite " )
2018-05-17 09:00:13 +00:00
click . echo ( )
click . echo ( " Enabling Certidude backend and nginx... " )
os . system ( " systemctl enable certidude " )
os . system ( " systemctl enable nginx " )
click . echo ( " To (re)start services: " )
click . echo ( )
click . echo ( " systemctl restart certidude " )
click . echo ( " systemctl restart nginx " )
click . echo ( )
2017-12-30 13:57:48 +00:00
return 0
2015-07-26 20:34:46 +00:00
2016-03-31 21:01:58 +00:00
@click.command ( " users " , help = " List users " )
def certidude_users ( ) :
from certidude . user import User
admins = set ( User . objects . filter_admins ( ) )
for user in User . objects . all ( ) :
2017-12-30 13:57:48 +00:00
click . echo ( " %s ; %s ; %s ; %s ; %s " % (
2016-03-31 21:01:58 +00:00
" admin " if user in admins else " user " ,
2017-12-30 13:57:48 +00:00
user . name , user . given_name , user . surname , user . mail ) )
2016-09-17 21:00:14 +00:00
2016-03-31 21:01:58 +00:00
2015-07-26 20:34:46 +00:00
@click.command ( " list " , help = " List certificates " )
2015-10-28 08:51:52 +00:00
@click.option ( " --verbose " , " -v " , default = False , is_flag = True , help = " Verbose output " )
2015-07-26 20:34:46 +00:00
@click.option ( " --show-key-type " , " -k " , default = False , is_flag = True , help = " Show key type and length " )
@click.option ( " --show-path " , " -p " , default = False , is_flag = True , help = " Show filesystem paths " )
@click.option ( " --show-extensions " , " -e " , default = False , is_flag = True , help = " Show X.509 Certificate Extensions " )
2015-10-28 08:51:52 +00:00
@click.option ( " --hide-requests " , " -h " , default = False , is_flag = True , help = " Hide signing requests " )
@click.option ( " --show-signed " , " -s " , default = False , is_flag = True , help = " Show signed certificates " )
@click.option ( " --show-revoked " , " -r " , default = False , is_flag = True , help = " Show revoked certificates " )
2015-12-12 22:34:08 +00:00
def certidude_list ( verbose , show_key_type , show_extensions , show_path , show_signed , show_revoked , hide_requests ) :
2015-10-28 08:51:52 +00:00
# Statuses:
# s - submitted
# v - valid
# e - expired
# y - not valid yet
# r - revoked
2017-04-13 20:52:09 +00:00
from humanize import naturaltime
2015-12-16 17:41:49 +00:00
from certidude import authority
2016-09-17 21:00:14 +00:00
2017-03-13 11:42:58 +00:00
def dump_common ( common_name , path , cert ) :
click . echo ( " certidude revoke %s " % common_name )
with open ( path , " rb " ) as fh :
buf = fh . read ( )
click . echo ( " md5sum: %s " % hashlib . md5 ( buf ) . hexdigest ( ) )
click . echo ( " sha1sum: %s " % hashlib . sha1 ( buf ) . hexdigest ( ) )
click . echo ( " sha256sum: %s " % hashlib . sha256 ( buf ) . hexdigest ( ) )
click . echo ( )
2015-10-28 08:51:52 +00:00
2015-12-12 22:34:08 +00:00
if not hide_requests :
2017-12-30 13:57:48 +00:00
for common_name , path , buf , csr , submitted , server in authority . list_requests ( ) :
2017-03-13 11:42:58 +00:00
created = 0
2015-12-12 22:34:08 +00:00
if not verbose :
2017-03-13 11:42:58 +00:00
click . echo ( " s " + path )
2015-12-12 22:34:08 +00:00
continue
2017-08-16 20:25:16 +00:00
click . echo ( )
2017-03-13 11:42:58 +00:00
click . echo ( click . style ( common_name , fg = " blue " ) )
click . echo ( " = " * len ( common_name ) )
click . echo ( " State: ? " + click . style ( " submitted " , fg = " yellow " ) + " " + naturaltime ( created ) + click . style ( " , %s " % created , fg = " white " ) )
click . echo ( " openssl req -in %s -text -noout " % path )
2017-03-28 09:24:51 +00:00
dump_common ( common_name , path , csr )
2017-03-13 11:42:58 +00:00
2015-12-12 22:34:08 +00:00
if show_signed :
2017-12-30 13:57:48 +00:00
for common_name , path , buf , cert , signed , expires in authority . list_signed ( ) :
2015-12-12 22:34:08 +00:00
if not verbose :
2017-08-16 20:25:16 +00:00
if signed < NOW and NOW < expires :
2017-03-13 11:42:58 +00:00
click . echo ( " v " + path )
2017-08-16 20:25:16 +00:00
elif expires < NOW :
2017-03-13 11:42:58 +00:00
click . echo ( " e " + path )
2015-10-28 08:51:52 +00:00
else :
2017-03-13 11:42:58 +00:00
click . echo ( " y " + path )
2015-12-12 22:34:08 +00:00
continue
2017-08-16 20:25:16 +00:00
click . echo ( )
2018-04-27 07:48:15 +00:00
click . echo ( click . style ( common_name , fg = " blue " ) + " " + click . style ( " %040x " % cert . serial_number , fg = " white " ) )
2017-03-13 11:42:58 +00:00
click . echo ( " = " * ( len ( common_name ) + 60 ) )
2017-08-16 20:25:16 +00:00
if signed < NOW and NOW < expires :
click . echo ( " Status: " + click . style ( " valid " , fg = " green " ) + " until " + naturaltime ( expires ) + click . style ( " , %s " % expires , fg = " white " ) )
elif NOW > expires :
click . echo ( " Status: " + click . style ( " expired " , fg = " red " ) + " " + naturaltime ( expires ) + click . style ( " , %s " % expires , fg = " white " ) )
2015-12-12 22:34:08 +00:00
else :
2017-08-16 20:25:16 +00:00
click . echo ( " Status: " + click . style ( " not valid yet " , fg = " red " ) + click . style ( " , %s " % expires , fg = " white " ) )
2015-12-12 22:34:08 +00:00
click . echo ( )
2017-03-13 11:42:58 +00:00
click . echo ( " openssl x509 -in %s -text -noout " % path )
dump_common ( common_name , path , cert )
2017-08-16 20:25:16 +00:00
for ext in cert [ " tbs_certificate " ] [ " extensions " ] :
2017-12-30 13:57:48 +00:00
click . echo ( " - %s : %s " % ( ext [ " extn_id " ] . native , repr ( ext [ " extn_value " ] . native ) ) )
2015-12-12 22:34:08 +00:00
if show_revoked :
2018-04-27 07:48:15 +00:00
for common_name , path , buf , cert , signed , expires , revoked , reason in authority . list_revoked ( ) :
2015-12-12 22:34:08 +00:00
if not verbose :
2017-03-13 11:42:58 +00:00
click . echo ( " r " + path )
2015-12-12 22:34:08 +00:00
continue
2017-08-16 20:25:16 +00:00
click . echo ( )
2018-04-27 07:48:15 +00:00
click . echo ( click . style ( common_name , fg = " blue " ) + " " + click . style ( " %040x " % cert . serial_number , fg = " white " ) )
2017-03-13 11:42:58 +00:00
click . echo ( " = " * ( len ( common_name ) + 60 ) )
2018-04-27 07:48:15 +00:00
click . echo ( " Status: " + click . style ( " revoked " , fg = " red " ) + " due to " + reason + " %s %s " % ( naturaltime ( NOW - revoked ) , click . style ( " , %s " % revoked , fg = " white " ) ) )
2017-03-13 11:42:58 +00:00
click . echo ( " openssl x509 -in %s -text -noout " % path )
dump_common ( common_name , path , cert )
2017-08-16 20:25:16 +00:00
for ext in cert [ " tbs_certificate " ] [ " extensions " ] :
2017-12-30 13:57:48 +00:00
click . echo ( " - %s : %s " % ( ext [ " extn_id " ] . native , repr ( ext [ " extn_value " ] . native ) ) )
2015-07-12 19:22:10 +00:00
2017-03-13 11:42:58 +00:00
@click.command ( " sign " , help = " Sign certificate " )
2015-07-26 20:34:46 +00:00
@click.argument ( " common_name " )
2018-04-16 12:13:31 +00:00
@click.option ( " --profile " , " -p " , default = " rw " , help = " Profile " )
2015-07-26 20:34:46 +00:00
@click.option ( " --overwrite " , " -o " , default = False , is_flag = True , help = " Revoke valid certificate with same CN " )
2018-03-03 13:54:31 +00:00
def certidude_sign ( common_name , overwrite , profile ) :
2018-04-27 07:48:15 +00:00
from certidude import authority , config
2017-08-16 20:25:16 +00:00
drop_privileges ( )
2018-04-16 12:13:31 +00:00
cert = authority . sign ( common_name , overwrite = overwrite , profile = config . PROFILES [ profile ] )
2017-03-13 11:42:58 +00:00
@click.command ( " revoke " , help = " Revoke certificate " )
2018-04-27 07:48:15 +00:00
@click.option ( " --reason " , " -r " , default = " key_compromise " , help = " Revocation reason, one of: key_compromise affiliation_changed superseded cessation_of_operation privilege_withdrawn " )
2017-03-13 11:42:58 +00:00
@click.argument ( " common_name " )
2018-04-27 07:48:15 +00:00
def certidude_revoke ( common_name , reason ) :
2017-03-13 11:42:58 +00:00
from certidude import authority
2017-08-16 20:25:16 +00:00
drop_privileges ( )
2018-04-27 07:48:15 +00:00
authority . revoke ( common_name , reason )
2017-03-13 11:42:58 +00:00
2015-07-26 20:34:46 +00:00
2017-12-30 13:57:48 +00:00
@click.command ( " expire " , help = " Move expired certificates " )
def certidude_expire ( ) :
2017-03-13 11:42:58 +00:00
from certidude import authority , config
2018-05-15 07:45:29 +00:00
threshold = datetime . utcnow ( ) - const . CLOCK_SKEW_TOLERANCE
2017-12-30 13:57:48 +00:00
for common_name , path , buf , cert , signed , expires in authority . list_signed ( ) :
if expires < threshold :
2018-04-27 07:48:15 +00:00
expired_path = os . path . join ( config . EXPIRED_DIR , " %040x .pem " % cert . serial_number )
2017-12-30 13:57:48 +00:00
click . echo ( " Moving %s to %s " % ( path , expired_path ) )
os . rename ( path , expired_path )
2018-04-27 07:48:15 +00:00
os . remove ( os . path . join ( config . SIGNED_BY_SERIAL_DIR , " %040x .pem " % cert . serial_number ) )
for common_name , path , buf , cert , signed , expires , revoked , reason in authority . list_revoked ( ) :
2017-12-30 13:57:48 +00:00
if expires < threshold :
2018-04-27 07:48:15 +00:00
expired_path = os . path . join ( config . EXPIRED_DIR , " %040x .pem " % cert . serial_number )
2017-12-30 13:57:48 +00:00
click . echo ( " Moving %s to %s " % ( path , expired_path ) )
2017-03-13 11:42:58 +00:00
os . rename ( path , expired_path )
2017-12-30 13:57:48 +00:00
# TODO: Send e-mail
2016-03-21 21:42:39 +00:00
2017-05-03 07:04:52 +00:00
2016-09-17 21:00:14 +00:00
@click.command ( " serve " , help = " Run server " )
2017-07-05 15:22:03 +00:00
@click.option ( " -p " , " --port " , default = 8080 , help = " Listen port " )
2017-07-05 21:22:02 +00:00
@click.option ( " -l " , " --listen " , default = " 127.0.1.1 " , help = " Listen address " )
2017-03-13 15:20:41 +00:00
@click.option ( " -f " , " --fork " , default = False , is_flag = True , help = " Fork to background " )
2017-07-11 18:57:19 +00:00
def certidude_serve ( port , listen , fork ) :
from certidude import authority , const , push
2016-09-17 21:00:14 +00:00
2017-07-05 15:22:03 +00:00
if port == 80 :
click . echo ( " WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx! " )
2018-01-03 22:12:02 +00:00
click . echo ( " Using configuration from: %s " % const . SERVER_CONFIG_PATH )
2017-05-03 07:04:52 +00:00
2017-04-04 05:02:08 +00:00
log_handlers = [ ]
2016-09-17 21:00:14 +00:00
2016-03-21 21:42:39 +00:00
from certidude import config
2018-05-15 07:45:29 +00:00
click . echo ( " OCSP responder subnets: %s " % config . OCSP_SUBNETS )
click . echo ( " CRL subnets: %s " % config . CRL_SUBNETS )
click . echo ( " SCEP subnets: %s " % config . SCEP_SUBNETS )
2018-04-16 12:13:31 +00:00
click . echo ( " Loading signature profiles: " )
for profile in config . PROFILES . values ( ) :
click . echo ( " - %s " % profile )
click . echo ( )
2017-05-25 19:20:29 +00:00
# Rebuild reverse mapping
2017-12-30 13:57:48 +00:00
for cn , path , buf , cert , signed , expires in authority . list_signed ( ) :
2018-04-27 07:48:15 +00:00
by_serial = os . path . join ( config . SIGNED_BY_SERIAL_DIR , " %040x .pem " % cert . serial_number )
2017-05-25 19:20:29 +00:00
if not os . path . exists ( by_serial ) :
click . echo ( " Linking %s to ../ %s .pem " % ( by_serial , cn ) )
os . symlink ( " ../ %s .pem " % cn , by_serial )
2017-05-01 22:32:55 +00:00
# Process directories
if not os . path . exists ( const . RUN_DIR ) :
click . echo ( " Creating: %s " % const . RUN_DIR )
os . makedirs ( const . RUN_DIR )
2017-12-30 13:57:48 +00:00
os . chmod ( const . RUN_DIR , 0o755 )
2017-05-01 23:06:45 +00:00
2016-03-21 21:42:39 +00:00
click . echo ( " Users subnets: %s " %
" , " . join ( [ str ( j ) for j in config . USER_SUBNETS ] ) )
click . echo ( " Administrative subnets: %s " %
" , " . join ( [ str ( j ) for j in config . ADMIN_SUBNETS ] ) )
click . echo ( " Auto-sign enabled for following subnets: %s " %
" , " . join ( [ str ( j ) for j in config . AUTOSIGN_SUBNETS ] ) )
click . echo ( " Request submissions allowed from following subnets: %s " %
" , " . join ( [ str ( j ) for j in config . REQUEST_SUBNETS ] ) )
2015-07-26 20:34:46 +00:00
2015-07-12 19:22:10 +00:00
click . echo ( " Serving API at %s : %d " % ( listen , port ) )
from wsgiref . simple_server import make_server , WSGIServer
2017-04-25 20:32:21 +00:00
from certidude . api import certidude_app
2015-07-12 19:22:10 +00:00
2015-07-27 12:30:50 +00:00
2015-07-12 19:22:10 +00:00
click . echo ( " Listening on %s : %d " % ( listen , port ) )
2015-07-26 20:34:46 +00:00
2017-04-25 21:10:12 +00:00
app = certidude_app ( log_handlers )
2017-05-07 19:11:24 +00:00
httpd = make_server ( listen , port , app , WSGIServer )
2015-09-03 09:00:45 +00:00
2016-09-17 21:00:14 +00:00
"""
Drop privileges
"""
2016-03-21 21:42:39 +00:00
2017-05-01 16:20:50 +00:00
# Initialize LDAP service ticket
if os . path . exists ( " /etc/cron.hourly/certidude " ) :
os . system ( " /etc/cron.hourly/certidude " )
2016-02-29 21:06:42 +00:00
2017-05-07 19:11:24 +00:00
from certidude . push import EventSourceLogHandler
log_handlers . append ( EventSourceLogHandler ( ) )
2016-09-17 21:00:14 +00:00
2017-04-04 05:02:08 +00:00
for j in logging . Logger . manager . loggerDict . values ( ) :
if isinstance ( j , logging . Logger ) : # PlaceHolder is what?
if j . name . startswith ( " certidude. " ) :
j . setLevel ( logging . DEBUG )
for handler in log_handlers :
j . addHandler ( handler )
2016-09-17 21:00:14 +00:00
2017-03-13 15:20:41 +00:00
if not fork or not os . fork ( ) :
2017-05-01 16:20:50 +00:00
pid = os . getpid ( )
with open ( const . SERVER_PID_PATH , " w " ) as pidfile :
pidfile . write ( " %d \n " % pid )
2017-07-11 18:57:19 +00:00
push . publish ( " server-started " )
2017-12-30 13:57:48 +00:00
logger . debug ( " Started Certidude at %s " , const . FQDN )
2017-05-09 09:48:24 +00:00
2017-07-11 18:57:19 +00:00
drop_privileges ( )
try :
httpd . serve_forever ( )
except KeyboardInterrupt :
2018-05-17 09:00:13 +00:00
click . echo ( " Caught Ctrl-C, exiting... " )
push . publish ( " server-stopped " )
logger . debug ( " Shutting down Certidude " )
return
2017-05-09 09:48:24 +00:00
2017-04-07 07:57:25 +00:00
@click.command ( " yubikey " , help = " Set up Yubikey as client authentication token " )
@click.argument ( " authority " )
@click.option ( " -p " , " --pin " , default = " 123456 " , help = " Slot pincode, 123456 by default " )
@click.option ( " -s " , " --slot " , default = " 9a " , help = " Yubikey slot to use, 9a by default " )
@click.option ( " -u " , " --username " , default = os . getenv ( " USER " ) , help = " Username to use, %s by default " % os . getenv ( " USER " ) )
def certidude_setup_yubikey ( authority , slot , username , pin ) :
2017-04-13 20:30:28 +00:00
import requests
2017-04-07 07:57:25 +00:00
cmd = " ykinfo " , " -q " , " -s "
click . echo ( " Executing: %s " % " " . join ( cmd ) )
serial = subprocess . check_output ( cmd ) . strip ( )
dn = " /CN= %s @yk- %s - %s " % ( username , slot , serial )
cmd = " yubico-piv-tool " , " -a " , " generate " , " -s " , slot , " -o " , " /tmp/pk.pem "
click . echo ( " Executing: %s " % " " . join ( cmd ) )
subprocess . call ( cmd )
cmd = " yubico-piv-tool " , \
" -i " , " /tmp/pk.pem " , " -o " , " /tmp/req.pem " , \
" -P " , pin , \
" -S " , dn , \
" -a " , " verify " , " -a " , " request " , \
" -s " , slot
click . echo ( " Executing: %s " % " " . join ( cmd ) )
scheme = " http "
request_url = " %s :// %s /api/request/?wait=true " % ( scheme , authority )
subprocess . check_output ( cmd )
click . echo ( " Submitting to %s , waiting for response... " % request_url )
headers = {
" Content-Type " : " application/pkcs10 " ,
" Accept " : " application/x-x509-user-cert,application/x-pem-file "
}
submission = requests . post ( request_url , data = open ( " /tmp/req.pem " ) , headers = headers )
with open ( " /tmp/cert.pem " , " w " ) as fh :
fh . write ( submission . text )
cmd = " yubico-piv-tool " , " -a " , " import-certificate " , " -s " , slot , " -i " , " /tmp/cert.pem "
click . echo ( " Executing: %s " % " " . join ( cmd ) )
subprocess . call ( cmd )
2017-04-21 16:58:01 +00:00
@click.command ( " test " , help = " Test mailer " )
@click.argument ( " recipient " )
def certidude_test ( recipient ) :
from certidude import mailer
mailer . send (
" test.md " ,
to = recipient
)
2018-05-15 07:45:29 +00:00
@click.command ( " list " , help = " List tokens " )
def certidude_token_list ( ) :
from certidude import config
from certidude . tokens import TokenManager
token_manager = TokenManager ( config . TOKEN_DATABASE )
cols = " uuid " , " expires " , " subject " , " state "
now = datetime . utcnow ( )
2018-05-17 09:00:13 +00:00
for token in token_manager . list ( expired = True , used = True ) :
2018-05-15 07:45:29 +00:00
token [ " state " ] = " used " if token . get ( " used " ) else ( " valid " if token . get ( " expires " ) > now else " expired " )
print ( " ; " . join ( [ str ( token . get ( col ) ) for col in cols ] ) )
@click.command ( " purge " , help = " Purge tokens " )
@click.option ( " -a " , " --all " , default = False , is_flag = True , help = " Purge all not only expired tokens " )
def certidude_token_purge ( all ) :
from certidude import config
from certidude . tokens import TokenManager
token_manager = TokenManager ( config . TOKEN_DATABASE )
print ( token_manager . purge ( all ) )
@click.command ( " issue " , help = " Issue token " )
@click.option ( " -m " , " --subject-mail " , default = None , help = " Subject e-mail override " )
@click.argument ( " subject " )
def certidude_token_issue ( subject , subject_mail ) :
from certidude import config
from certidude . tokens import TokenManager
from certidude . user import User
token_manager = TokenManager ( config . TOKEN_DATABASE )
token_manager . issue ( None , User . objects . get ( subject ) , subject_mail )
2017-04-21 16:58:01 +00:00
2015-08-13 08:11:08 +00:00
@click.group ( " strongswan " , help = " strongSwan helpers " )
def certidude_setup_strongswan ( ) : pass
2015-07-26 20:34:46 +00:00
@click.group ( " openvpn " , help = " OpenVPN helpers " )
def certidude_setup_openvpn ( ) : pass
2015-07-12 19:22:10 +00:00
2015-07-26 20:34:46 +00:00
@click.group ( " setup " , help = " Getting started section " )
def certidude_setup ( ) : pass
2015-07-12 19:22:10 +00:00
2018-05-15 07:45:29 +00:00
@click.group ( " token " , help = " Token management " )
def certidude_token ( ) : pass
2015-07-12 19:22:10 +00:00
@click.group ( )
def entry_point ( ) : pass
2015-08-13 08:11:08 +00:00
certidude_setup_strongswan . add_command ( certidude_setup_strongswan_server )
certidude_setup_strongswan . add_command ( certidude_setup_strongswan_client )
2015-10-17 15:07:26 +00:00
certidude_setup_strongswan . add_command ( certidude_setup_strongswan_networkmanager )
2015-07-26 20:34:46 +00:00
certidude_setup_openvpn . add_command ( certidude_setup_openvpn_server )
certidude_setup_openvpn . add_command ( certidude_setup_openvpn_client )
2016-03-27 20:38:14 +00:00
certidude_setup_openvpn . add_command ( certidude_setup_openvpn_networkmanager )
2015-07-26 20:34:46 +00:00
certidude_setup . add_command ( certidude_setup_authority )
certidude_setup . add_command ( certidude_setup_openvpn )
2015-08-13 08:11:08 +00:00
certidude_setup . add_command ( certidude_setup_strongswan )
2016-03-21 21:42:39 +00:00
certidude_setup . add_command ( certidude_setup_nginx )
2017-04-07 07:57:25 +00:00
certidude_setup . add_command ( certidude_setup_yubikey )
2018-05-15 07:45:29 +00:00
certidude_token . add_command ( certidude_token_list )
certidude_token . add_command ( certidude_token_purge )
certidude_token . add_command ( certidude_token_issue )
entry_point . add_command ( certidude_token )
2015-07-26 20:34:46 +00:00
entry_point . add_command ( certidude_setup )
entry_point . add_command ( certidude_serve )
2017-12-30 13:57:48 +00:00
entry_point . add_command ( certidude_enroll )
2015-07-26 20:34:46 +00:00
entry_point . add_command ( certidude_sign )
2017-03-13 11:42:58 +00:00
entry_point . add_command ( certidude_revoke )
2015-07-26 20:34:46 +00:00
entry_point . add_command ( certidude_list )
2017-12-30 13:57:48 +00:00
entry_point . add_command ( certidude_expire )
2016-03-31 21:01:58 +00:00
entry_point . add_command ( certidude_users )
2017-04-21 16:58:01 +00:00
entry_point . add_command ( certidude_test )
2016-09-17 21:00:14 +00:00
if __name__ == " __main__ " :
entry_point ( )