diff --git a/warden3/contrib/warden_ra/README b/warden3/contrib/warden_ra/README new file mode 100644 index 0000000000000000000000000000000000000000..9f6789f575960bd9bdc4661abc3c8039dd9c227c --- /dev/null +++ b/warden3/contrib/warden_ra/README @@ -0,0 +1,233 @@ +Warden Registration Authority for Warden 3.X +============================================ + +Introduction +------------ + +Warden RA is a certificate registration authority for Warden_ server. +It is meant to support the client registration process and simplification +of the credential transport. + +As Warden clients are authenticated by X509 certificate, the usual certificate +generation process can be used - local key and certificate request gets +generated, the request is submitted to registration authority, and after +review, certificate is issued and delivered back. + +However in centralised Warden setup, it is useful to be able to preallow +certificate for particular client during negotiation phase (thus removing +another round-trip). + +This is done by issuing 'token' by Warden registration officer during client +registration, which is just a oneshot password, allowing sending the request +and getting new certificate in one step through web API. + +Password is meant only for completely new clients or unusual situations, +however RA also allows prolongation - generating new certificate by using +old certificate (which must be still valid, of course) instead of password. + +The application script, which can be distributed to newly registered clients, +is also included. + +Dependencies +------------ + + 1. Platform + + Python 2.7+ + Apache 2.2+ + mod_wsgi 3.3+ + EJBCA_ 3.9+ + + +Registration process +-------------------- + +New client credentials +`````````````````````` + +After succesful negotiation of new Warden client parameters, the registration +officer enables new certificate generation by issuing (on the server side): + + warden_ra.py applicant --name org.example.warden.client + +The tool generates and sets one time password on the registration authority +side, and this password can be sent (preferably through the secured channel) +to the new client administrator along with other setup information. + +The client administrator runs the application script with application +password: + + warden_apply.sh org.example.warden.client P4SSW0RD + +The script creates new X509 key, CSR certificate request and makes call to +the Warden RA web service, where it obtains the new complete certificate. + + +Prolonging existing client credentials +`````````````````````````````````````` + +The client administrator runs the application script with his existing valid +Warden credentials, which he needs to prolong: + + warden_apply.sh org.example.warden.client cert.pem key.pem + +The script creates new X509 key, CSR certificate request and makes call to +the Warden RA web service, where it obtains the new complete certificate. + + +Installation +------------ + +As for now, correctly configured and running EJBCA_ PKI is necessary. PKI part +of the RA is however pluggable, so simple openssl backend is also planned. + + +This depends heavily on your distribution and Apache configuration. +Basically you need to create and include apache.conf: + + Include /opt/warden_ra/apache22.conf + +or paste the contents into whichever Directory, Location or VirtualHost +you dedicate for Warden RA. Note that you have to use different host than +the one for Warden server, as Warden RA needs different Apache options +for SSL authentication. + +You may need to change paths to certificate/key/ca material, path to +warden_server.wsgi and web path alias. + +Note that warden_ra itself is NOT thread safe, so included configuration +expects Apache with mpm-prefork worker, or you will have to configure +mod_wsgi as separate process with threads=1. + +Also, for warden_server.wsgi, you can use warden_server.wsgi.dist as +a template. You will possibly need to change at least configuration +file path. + + * Configure Warden RA (see next chapter) + + * Reload Apache + + +Configuration +------------- + + Configuration is JSON object in file (warden_server.cfg by default), +however, lines starting with "#" or "//" are allowed and will be ignored as +comments. File must contain valid JSON object, containing configuration. See +also warden_server.cfg.dist as example. + + Top level JSON object contains subsections, which configure particular +aspect of the server. Each subsection can contain "type" key, which chooses +particular implementation object of the aspect, for example type of logger +(file, syslog), such as: + + { + "Log": { + "type": "SysLogger", + ... + }, + "DB": { ... } + } + +Sections and their "type" objects can be: + + Log: FileLogger, SysLogger + Auth: OptionalAuthenticator + Registry: EjbcaRegistry + Handler: CertHandler + +"type" keyword is not mandatory, if not specified, first implementation +object from particular section list is used ("FileLogger" for example). + +Object function and configuration keys are as follows: + + FileLogger: logging into file on the filesystem + filename: name of the file, defaults to "warden_ra.log" at + installation directory + level: least log level (CRITICAL, ERROR, WARNING, INFO, DEBUG) + + SysLogger: logging into unix syslog + socket: path to syslog socket, defaults to "/dev/log" + facility: syslog facility, defaults to "daemon" + level: least log level (CRITICAL, ERROR, WARNING, INFO, DEBUG) + + OptionalAuthenticator: authenticate based on X509 certificate, or + signal the password auth for the registry + + CertHandler: the main certificate requestor implementation + + EjbcaRegistry: EJBCA connector configuration + url: EJBCA API URL, for example "https://ejbca.example.org/ejbca/ejbcaws/ejbcaws?wsdl" + cert: certificate for authentication to EJBCA, defaults to "warden_ra.cert.pem" + key: key for authentication to EJBCA, defaults to "warden_ra.key.pem" + ca_name: name of the CA, dedicated for Warden, defaults to "Example CA" + certificate_profile_name: name of the EJBCA certificate profile, defaults to "Example" + end_entity_profile_name: name of the EJBCA entity profile, defaults to "Example EE" + subject_dn_template: template for the DN generation, defaults to "DC=cz,DC=example-ca,DC=warden,CN=%s" + username_suffix: suffix, which will be added to EJBCA entities, defaults to "@warden" + + +Command line +------------ + +Whe run from the command line, RA allows for client and request management. + + + warden_ra.py [--help] [-c CONFIG] [-v] + {list,register,applicant,request,gencert} ... + + Warden server certificate registry + + arguments: + --help show this help message and exit + -c CONFIG, --config CONFIG + path to configuration file + -v, --verbose be more chatty + + commands: + {list,register,applicant,request,gencert} + list list clients + register register client + applicant allow for certificate application + request generate CSR + gencert get new certificate + + + warden_ra.py list [--help] [--name NAME] + + List registered clients. + + arguments: + --help show this help message and exit + --name NAME client name + + + warden_ra.py register [--help] --name NAME --admins + [ADMINS [ADMINS ...]] + + Add client registration entry. + + arguments: + --help show this help message and exit + --name NAME client name + --admins [ADMINS [ADMINS ...]] + administrator list + + + warden_ra.py applicant [--help] --name NAME [--password PASSWORD] + + Set client into certificate application mode and set its password + + optional arguments: + --help show this help message and exit + --name NAME client name + --password PASSWORD password for application (will be autogenerated if not + set) + + +.. _Warden: https://warden.cesnet.cz/ +.. _EJBCA: https://www.ejbca.org/ + +------------------------------------------------------------------------------ + +Copyright (C) 2017 Cesnet z.s.p.o diff --git a/warden3/contrib/warden_ra/apache22.conf b/warden3/contrib/warden_ra/apache22.conf index cea3317dacfb34b47d6e97cef353d7a1e8753581..93ca7f805491066748f5393d210edd3687022eeb 100644 --- a/warden3/contrib/warden_ra/apache22.conf +++ b/warden3/contrib/warden_ra/apache22.conf @@ -1,6 +1,6 @@ SSLEngine on -SSLVerifyClient none +SSLVerifyClient optional SSLOptions +StdEnvVars +ExportCertData SSLCertificateFile /opt/warden_server_3/cert.pem diff --git a/warden3/contrib/warden_ra/warden_apply.sh b/warden3/contrib/warden_ra/warden_apply.sh old mode 100755 new mode 100644 index 1992a981761f5672b98d7331032cc6d9cf1e5d68..bb9bae46132e2e2320e27c37a86d8bbbcb2c1777 --- a/warden3/contrib/warden_ra/warden_apply.sh +++ b/warden3/contrib/warden_ra/warden_apply.sh @@ -8,12 +8,14 @@ result=${TMPDIR:-${TMP:-/tmp}}/cert.$$.$RANDOM config=${TMPDIR:-${TMP:-/tmp}}/conf.$$.$RANDOM client="$1" password="$2" +incert="$2" +inkey="$3" trap 'rm -f "$config $result"' INT TERM HUP EXIT function flee { echo -e "$1"; exit $2; } -[ -z "$client" -o -z "$password" ] && flee "Usage: ${0%.*} client.name password" 255 +[ -z "$client" -o -z "$password" ] && flee "Usage: ${0%.*} client.name password\n ${0%.*} client.name cert_file key_file" 255 for n in openssl curl; do command -v "$n" 2>&1 >/dev/null || flee "Haven't found $n binary." 251 @@ -29,7 +31,11 @@ echo -e "default_bits=2048\ndistinguished_name=rdn\nprompt=no\n[rdn]\ncommonName openssl req -new -nodes -batch -keyout "$key" -out "$csr" -config "$config" || flee "Error generating key/certificate request." 252 -curl --progress-bar --request POST --data-binary '@-' "$url?name=$client&password=$password" < "$csr" > "$result" +if [ -z "$inkey" ]; then + curl --progress-bar --request POST --data-binary '@-' "$url?name=$client&password=$password" < "$csr" > "$result" +else + curl --progress-bar --request POST --data-binary '@-' --cert "$incert" --key "$inkey" "$url?name=$client" < "$csr" > "$result" +fi case $(<$result) in '-----BEGIN CERTIFICATE-----'*) mv "$result" "$cert" diff --git a/warden3/contrib/warden_ra/warden_ra.py b/warden3/contrib/warden_ra/warden_ra.py index 2ae698ce2a9a2eacbf832f4e5a354b27284728f7..9437bf17f6bc376fd9910426976a3498b1d5003a 100755 --- a/warden3/contrib/warden_ra/warden_ra.py +++ b/warden3/contrib/warden_ra/warden_ra.py @@ -167,7 +167,7 @@ def format_cert(cert): # Server side -class NullAuthenticator(ObjectBase): +class OptionalAuthenticator(ObjectBase): def __init__(self, req, log): ObjectBase.__init__(self, req, log) @@ -178,7 +178,30 @@ class NullAuthenticator(ObjectBase): def authenticate(self, env, args): - return True + cert_name = env.get("SSL_CLIENT_S_DN_CN") + + if cert_name: + if cert_name != args.setdefault("name", [cert_name])[0]: + exception = self.req.error(message="authenticate: client name does not correspond with certificate", error=403, cn = cert_name, args = args) + exception.log(self.log) + return None + + verify = env.get("SSL_CLIENT_VERIFY") + if verify != "SUCCESS": + exception = self.req.error(message="authenticate: certificate present but verification failed", error=403, cn = cert_name, args = args, verify=verify) + exception.log(self.log) + return None + + return "cert" # Ok, client authorized by valid certificate + + else: + try: + args["password"][0] + return "pwd" # Ok, pass on, but getCert will have to rely on certificate registry password + except KeyError, IndexError: + exception = self.req.error(message="authenticate: no certificate nor password present", error=403, cn = cert_name, args = args) + exception.log(self.log) + return None def authorize(self, env, client, path, method): @@ -193,12 +216,21 @@ class CertHandler(ObjectBase): @expose(read=1, debug=1) def getCert(self, csr_data=None, name=None, password=None): - if not (name and password and csr_data): - raise self.req.error(message="Wrong or missing arguments", error=400, client=name[0], password=password[0]) + if not (name and csr_data): + raise self.req.error(message="Wrong or missing arguments", error=400, name=name, password=password) client = self.registry.get_client(name[0]) if not client: - raise self.req.error(message="Unknown client", error=403, client=name[0], password=password[0]) - self.log.info("Client: %s" % client.name) + raise self.req.error(message="Unknown client", error=403, name=name, password=password) + self.log.info("Client %s" % client.name) + if self.req.client == "cert": + # Correctly authenticated by cert, most probably not preactivated with password, + # so generate oneshot password and allow now + password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16))) + self.log.debug("Authorized by X509, enabling cert generation with password %s" % password) + client.allow_new_cert(pwd=password) + client.save() + if not password: + raise self.req.error(message="Missing password and certificate validation failed", error=403, name=name, password=password) try: newcert = client.new_cert(csr_data, password) except Exception as e: @@ -215,7 +247,7 @@ section_order = ("log", "auth", "registry", "handler", "server") # "type" keyword in section may be used to choose other section_def = { "log": [FileLogger, SysLogger], - "auth": [NullAuthenticator], + "auth": [OptionalAuthenticator], "registry": [EjbcaRegistry], "handler": [CertHandler], "server": [Server] @@ -226,7 +258,7 @@ param_def = { FileLogger: warden_server.param_def[FileLogger], SysLogger: warden_server.param_def[SysLogger], Server: warden_server.param_def[Server], - NullAuthenticator: { + OptionalAuthenticator: { "req": {"type": "obj", "default": "req"}, "log": {"type": "obj", "default": "log"} },