diff --git a/certidude/cli.py b/certidude/cli.py index 3d9168f..a438ad4 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -12,9 +12,9 @@ import socket import string import subprocess import sys +from base64 import b64encode from configparser import ConfigParser, NoOptionError, NoSectionError -from certidude.helpers import certidude_request_certificate -from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges +from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup from datetime import datetime, timedelta from time import sleep import const @@ -93,11 +93,15 @@ def setup_client(prefix="client_", dh=False): def certidude_request(fork, renew, no_wait, system_keytab_required): # Here let's try to avoid compiling packages from scratch rpm("openssl") or \ - apt("openssl python-cryptography python-jinja2") or \ - pip("cryptography jinja2") + apt("openssl python-jinja2") or \ + pip("jinja2 oscrypto csrbuilder asn1crypto") import requests from jinja2 import Environment, PackageLoader + from oscrypto import asymmetric + from asn1crypto import crl, pem + from csrbuilder import CSRBuilder, pem_armor_csr + env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) if not os.path.exists(const.CLIENT_CONFIG_PATH): @@ -129,52 +133,30 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): fh.write(env.get_template("client/certidude.service").render(context)) - for authority in clients.sections(): - try: - endpoint_renewal_overlap = clients.getint(authority, "renewal overlap") - except NoOptionError: - endpoint_renewal_overlap = None - try: - endpoint_insecure = clients.getboolean(authority, "insecure") - except NoOptionError: - endpoint_insecure = False - try: - endpoint_common_name = clients.get(authority, "common name") - except NoOptionError: - endpoint_common_name = const.HOSTNAME - try: - endpoint_key_path = clients.get(authority, "key path") - except NoOptionError: - endpoint_key_path = "/var/lib/certidude/%s/keys/%s.pem" % (authority, const.HOSTNAME) - try: - endpoint_request_path = clients.get(authority, "request path") - except NoOptionError: - endpoint_request_path = "/var/lib/certidude/%s/requests/%s.pem" % (authority, const.HOSTNAME) - try: - endpoint_certificate_path = clients.get(authority, "certificate path") - except NoOptionError: - endpoint_certificate_path = "/var/lib/certidude/%s/signed/%s.pem" % (authority, const.HOSTNAME) - try: - endpoint_authority_path = clients.get(authority, "authority path") - except NoOptionError: - endpoint_authority_path = "/var/lib/certidude/%s/ca_crt.pem" % authority - try: - endpoint_revocations_path = clients.get(authority, "revocations path") - except NoOptionError: - endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority + for authority_name in clients.sections(): # TODO: Create directories automatically - if clients.get(authority, "trigger") == "domain joined": - system_keytab_required = True - elif clients.get(authority, "trigger") != "interface up": - continue + try: + trigger = clients.get(authority_name, "trigger") + except NoOptionError: + trigger = "interface up" - if system_keytab_required: + if trigger == "domain joined": # Stop further processing if command line argument said so or trigger expects domain membership if not os.path.exists("/etc/krb5.keytab"): continue + use_keytab = True + elif trigger == "interface up": + pass + else: + raise - pid_path = os.path.join(const.RUN_DIR, authority + ".pid") + + ######################### + ### Fork if requested ### + ######################### + + pid_path = os.path.join(const.RUN_DIR, authority_name + ".pid") try: with open(pid_path) as fh: @@ -194,34 +176,239 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): 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()) - retries = 30 - while retries > 0: + try: + scheme = "http" if clients.getboolean(authority_name, "insecure") else "https" + except NoOptionError: + scheme = "https" + + # Expand ca.example.com + authority_url = "%s://%s/api/certificate/" % (scheme, authority_name) + request_url = "%s://%s/api/request/" % (scheme, authority_name) + revoked_url = "%s://%s/api/revoked/" % (scheme, authority_name) + + + try: + authority_path = clients.get(authority_name, "authority path") + except NoOptionError: + authority_path = "/var/lib/certidude/%s/ca_crt.pem" % authority_name + finally: + if os.path.exists(authority_path): + click.echo("Found authority certificate in: %s" % authority_path) + else: + if not os.path.exists(os.path.dirname(authority_path)): + os.makedirs(os.path.dirname(authority_path)) + 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"}) + asymmetric.load_certificate(r.content) + except: + raise + # raise ValueError("Failed to parse PEM: %s" % r.text) + authority_partial = authority_path + ".part" + with open(authority_partial, "w") as oh: + oh.write(r.content) + click.echo("Writing authority certificate to: %s" % authority_path) + selinux_fixup(authority_partial) + os.rename(authority_partial, authority_path) + + + # 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 + + + ############### + ### Get CRL ### + ############### + + try: + revocations_path = clients.get(authority_name, "revocations path") + except NoOptionError: + revocations_path = None + else: + # Fetch certificate revocation list + click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path)) + r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'}) + assert r.status_code == 200, "Failed to fetch CRL from %s, got %s" % (revoked_url, r.text) + + #revocations = crl.CertificateList.load(pem.unarmor(r.content)) + # 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) + + 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 + + ################################ + ### Generate keypair and CSR ### + ################################ + + try: + key_path = clients.get(authority_name, "key path") + request_path = clients.get(authority_name, "request path") + except NoOptionError: + key_path = "/var/lib/certidude/%s/client_key.pem" % authority_name + request_path = "/var/lib/certidude/%s/client_csr.pem" % authority_name + + if not os.path.exists(request_path): + key_partial = key_path + ".part" + request_partial = request_path + ".part" + public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048) + builder = CSRBuilder({u"common_name": common_name}, public_key) + 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) + + ############################################## + ### Submit CSR and save signed certificate ### + ############################################## + + try: + certificate_path = clients.get(authority_name, "certificate path") + except NoOptionError: + certificate_path = "/var/lib/certidude/%s/client_cert.pem" % authority_name + + headers={ + "Content-Type": "application/pkcs10", + "Accept": "application/x-x509-user-cert,application/x-pem-file" + } + + + try: + # Attach renewal signature if renewal requested and cert exists + renewal_overlap = clients.getint(authority_name, "renewal overlap") + with open(certificate_path) as ch, open(request_path) as rh, open(key_path) as kh: + cert_buf = ch.read() + cert = asymmetric.load_certificate(cert_buf) + expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native + if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap): + click.echo("Certificate will expire %s, will attempt to renew" % expires) + renew = True + headers["X-Renewal-Signature"] = b64encode( + asymmetric.rsa_pss_sign( + asymmetric.load_private_key(kh.read()), + cert_buf + rh.read(), + "sha512")) + except NoOptionError: # Renewal not specified in config + pass + except EnvironmentError: # Certificate missing + pass + else: + click.echo("Attached renewal signature %s" % headers["X-Renewal-Signature"]) + + if not os.path.exists(certificate_path) or renew: + # Set up URL-s + request_params = set() + request_params.add("autosign=true") + if not no_wait: + request_params.add("wait=forever") + if request_params: + request_url = request_url + "?" + "&".join(request_params) + + # If machine is joined to domain attempt to present machine credentials for authentication + if use_keytab: + 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 TGT using machine keytab") + assert os.path.exists("/tmp/ca.ticket"), "Ticket not created!" + click.echo("Initialized Kerberos TGT using machine keytab") + from requests_kerberos import HTTPKerberosAuth, OPTIONAL + auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) + else: + click.echo("Not using machine keytab") + auth = None + + submission = requests.post(request_url, auth=auth, data=open(request_path), headers=headers) + + # 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) + return + 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 + else: + submission.raise_for_status() + try: - certidude_request_certificate( - authority, - system_keytab_required, - endpoint_key_path, - endpoint_request_path, - endpoint_certificate_path, - endpoint_authority_path, - endpoint_revocations_path, - endpoint_common_name, - endpoint_renewal_overlap, - insecure=endpoint_insecure, - autosign=True, - wait=not no_wait, - renew=renew) - break - except requests.exceptions.Timeout: - retries -= 1 - continue + cert = asymmetric.load_certificate(submission.content) + except: # TODO: catch correct exceptions + raise ValueError("Failed to parse PEM: %s" % submission.text) + + 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) + + + ################################## + ### Configure related services ### + ################################## for endpoint in service_config.sections(): - if service_config.get(endpoint, "authority") != authority: + if service_config.get(endpoint, "authority") != authority_name: continue click.echo("Configuring '%s'" % endpoint) @@ -256,7 +443,7 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): # Identify correct ipsec.conf section by leftcert if section_type != "conn": continue - if config[section_type,section_name]["leftcert"] != endpoint_certificate_path: + if config[section_type,section_name]["leftcert"] != certificate_path: continue if config[section_type,section_name].get("left", "") == "%defaultroute": @@ -285,14 +472,6 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): # OpenVPN set up with NetworkManager if service_config.get(endpoint, "service") == "network-manager/openvpn": - try: - endpoint_port = service_config.getint(endpoint, "port") - except NoOptionError: - endpoint_port = 1194 - try: - endpoint_proto = service_config.get(endpoint, "proto") - except NoOptionError: - endpoint_proto = "udp" # NetworkManager-strongswan-gnome nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint) if os.path.exists(nm_config_path): @@ -300,6 +479,7 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): continue nm_config = ConfigParser() nm_config.add_section("connection") + nm_config.set("connection", "certidude managed", "true") nm_config.set("connection", "id", endpoint) nm_config.set("connection", "uuid", uuid) nm_config.set("connection", "type", "vpn") @@ -311,18 +491,26 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): nm_config.set("vpn", "tap-dev", "no") 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")) - nm_config.set("vpn", "port", str(endpoint_port)) - if endpoint_proto == "tcp": - nm_config.set("vpn", "proto-tcp", "yes") - nm_config.set("vpn", "key", endpoint_key_path) - nm_config.set("vpn", "cert", endpoint_certificate_path) - nm_config.set("vpn", "ca", endpoint_authority_path) + nm_config.set("vpn", "key", key_path) + nm_config.set("vpn", "cert", certificate_path) + nm_config.set("vpn", "ca", authority_path) 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") + 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 + # Prevent creation of files with liberal permissions os.umask(0o177) @@ -336,10 +524,11 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): # IPSec set up with NetworkManager - elif service_config.get(endpoint, "service") == "network-manager/strongswan": + if service_config.get(endpoint, "service") == "network-manager/strongswan": client_config = ConfigParser() nm_config = ConfigParser() nm_config.add_section("connection") + nm_config.set("connection", "certidude managed", "true") nm_config.set("connection", "id", endpoint) nm_config.set("connection", "uuid", uuid) nm_config.set("connection", "type", "vpn") @@ -350,9 +539,9 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): nm_config.set("vpn", "method", "key") nm_config.set("vpn", "ipcomp", "no") nm_config.set("vpn", "address", service_config.get(endpoint, "remote")) - nm_config.set("vpn", "userkey", endpoint_key_path) - nm_config.set("vpn", "usercert", endpoint_certificate_path) - nm_config.set("vpn", "certificate", endpoint_authority_path) + nm_config.set("vpn", "userkey", key_path) + nm_config.set("vpn", "usercert", certificate_path) + nm_config.set("vpn", "certificate", authority_path) nm_config.add_section("ipv4") nm_config.set("ipv4", "method", "auto") diff --git a/certidude/common.py b/certidude/common.py index 16c6a33..325df70 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -3,6 +3,15 @@ import os import click import subprocess +def selinux_fixup(path): + """ + Fix OpenVPN credential store security context on Fedora + """ + if not os.path.exists("/sys/fs/selinux"): + return + cmd = "chcon", "--type=home_cert_t", path + subprocess.call(cmd) + def drop_privileges(): from certidude import config import pwd diff --git a/certidude/const.py b/certidude/const.py index c8222b6..b9d6961 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -23,7 +23,11 @@ except socket.gaierror: click.echo("Failed to resolve fully qualified hostname of this machine, make sure hostname -f works") sys.exit(254) -HOSTNAME, DOMAIN = FQDN.split(".", 1) +try: + HOSTNAME, DOMAIN = FQDN.split(".", 1) +except ValueError: # If FQDN is not configured + HOSTNAME = FQDN + DOMAIN = None # TODO: lazier, otherwise gets evaluated before installing package if os.path.exists("/etc/strongswan/ipsec.conf"): # fedora dafuq?! diff --git a/certidude/helpers.py b/certidude/helpers.py deleted file mode 100644 index c3aa16b..0000000 --- a/certidude/helpers.py +++ /dev/null @@ -1,272 +0,0 @@ - -import click -import os -import subprocess -import tempfile -from base64 import b64encode -from datetime import datetime, timedelta -from configparser import ConfigParser - -def selinux_fixup(path): - """ - Fix OpenVPN credential store security context on Fedora - """ - if not os.path.exists("/sys/fs/selinux"): - return - cmd = "chcon", "--type=home_cert_t", path - subprocess.call(cmd) - -def certidude_request_certificate(authority, system_keytab_required, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, renewal_overlap, autosign=False, wait=False, bundle=False, renew=False, insecure=False): - """ - Exchange CSR for certificate using Certidude HTTP API server - """ - import requests - from certidude import errors, const - from cryptography import x509 - from cryptography.hazmat.primitives.asymmetric import rsa, padding - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.serialization import Encoding - from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID - - # Create directories - for path in key_path, request_path, certificate_path, authority_path, revocations_path: - dir_path = os.path.dirname(path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - # Set up URL-s - request_params = set() - if autosign: - request_params.add("autosign=true") - if wait: - request_params.add("wait=forever") - - # Expand ca.example.com - scheme = "http" if insecure else "https" # TODO: Expose in CLI - authority_url = "%s://%s/api/certificate/" % (scheme, authority) - request_url = "%s://%s/api/request/" % (scheme, authority) - revoked_url = "%s://%s/api/revoked/" % (scheme, authority) - - if request_params: - request_url = request_url + "?" + "&".join(request_params) - - if os.path.exists(authority_path): - click.echo("Found authority certificate in: %s" % authority_path) - else: - 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"}) - x509.load_pem_x509_certificate(r.content, default_backend()) - except: - raise - # raise ValueError("Failed to parse PEM: %s" % r.text) - authority_partial = tempfile.mktemp(prefix=authority_path + ".part") - with open(authority_partial, "w") as oh: - oh.write(r.content) - click.echo("Writing authority certificate to: %s" % authority_path) - selinux_fixup(authority_partial) - os.rename(authority_partial, authority_path) - - # Fetch certificate revocation list - r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'}, stream=True) - assert r.status_code == 200, "Failed to fetch CRL from %s, got %s" % (revoked_url, r.text) - click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path)) - revocations_partial = tempfile.mktemp(prefix=revocations_path + ".part") - with open(revocations_partial, 'wb') as f: - for chunk in r.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - if subprocess.call(("openssl", "crl", "-CAfile", authority_path, "-in", revocations_partial, "-noout")): - raise ValueError("Failed to verify CRL in %s" % revocations_partial) - else: - # TODO: Check monotonically increasing CRL number - click.echo("Certificate revocation list passed verification") - selinux_fixup(revocations_partial) - os.rename(revocations_partial, revocations_path) - - # Check if we have been inserted into CRL - if os.path.exists(certificate_path): - cert = x509.load_pem_x509_certificate(open(certificate_path).read(), default_backend()) - - for revocation in x509.load_pem_x509_crl(open(revocations_path).read(), default_backend()): - extension, = revocation.extensions - - if revocation.serial_number == cert.serial: - if extension.value.reason == x509.ReasonFlags.certificate_hold: - # Don't do anything for now - # TODO: disable service - break - - # Disable the client if operation has been ceased - if extension.value.reason == x509.ReasonFlags.cessation_of_operation: - if os.path.exists("/etc/certidude/client.conf"): - clients.readfp(open("/etc/certidude/client.conf")) - if clients.has_section(authority): - clients.set(authority, "trigger", "operation ceased") - clients.write(open("/etc/certidude/client.conf", "w")) - click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf") - # TODO: Disable related services - return - - click.echo("Certificate has been revoked, wiping keys and certificates!") - if os.path.exists(key_path): - os.remove(key_path) - if os.path.exists(request_path): - os.remove(request_path) - if os.path.exists(certificate_path): - os.remove(certificate_path) - break - else: - click.echo("Certificate does not seem to be revoked. Good!") - - - try: - request_buf = open(request_path).read() - request = x509.load_pem_x509_csr(request_buf, default_backend()) - click.echo("Found signing request: %s" % request_path) - with open(key_path) as fh: - key = serialization.load_pem_private_key( - fh.read(), - password=None, - backend=default_backend()) - except EnvironmentError: - - # Construct private key - click.echo("Generating %d-bit RSA key..." % const.KEY_SIZE) - key = rsa.generate_private_key( - public_exponent=65537, - key_size=const.KEY_SIZE, - backend=default_backend() - ) - - # Dump private key - key_partial = tempfile.mktemp(prefix=key_path + ".part") - os.umask(0o077) - with open(key_partial, "wb") as fh: - fh.write(key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - )) - - # Set subject name attributes - names = [x509.NameAttribute(NameOID.COMMON_NAME, common_name.decode("utf-8"))] - - # Construct CSR - csr = x509.CertificateSigningRequestBuilder( - ).subject_name(x509.Name(names)) - - # Sign & dump CSR - os.umask(0o022) - request_partial = tempfile.mktemp(prefix=request_path + ".part") - with open(request_partial, "wb") as f: - f.write(csr.sign(key, hashes.SHA256(), default_backend()).public_bytes(serialization.Encoding.PEM)) - - click.echo("Writing private key to: %s" % key_path) - selinux_fixup(key_partial) - os.rename(key_partial, key_path) - - click.echo("Writing certificate signing request to: %s" % request_path) - os.rename(request_partial, request_path) - - # We have CSR now, save the paths to client.conf so we could: - # Update CRL, renew certificate, maybe something extra? - - if os.path.exists(certificate_path): - cert_buf = open(certificate_path).read() - cert = x509.load_pem_x509_certificate(cert_buf, default_backend()) - lifetime = (cert.not_valid_after - cert.not_valid_before) - if renewal_overlap and datetime.now() > cert.not_valid_after - timedelta(days=renewal_overlap): - click.echo("Certificate will expire %s, will attempt to renew" % cert.not_valid_after) - renew = True - else: - click.echo("Found valid certificate: %s" % certificate_path) - if not renew: # Don't do anything if renewal wasn't requested explicitly - return - else: - cert = None - - # If machine is joined to domain attempt to present machine credentials for authentication - if system_keytab_required: - os.environ["KRB5CCNAME"]="/tmp/ca.ticket" - - # Mac OS X has keytab with lowercase hostname - cmd = "kinit -S HTTP/%s -k %s$" % (authority, 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, const.HOSTNAME.upper()) - if os.system(cmd): - # Failed, probably /etc/krb5.keytab contains spaghetti - raise ValueError("Failed to initialize TGT using machine keytab") - assert os.path.exists("/tmp/ca.ticket"), "Ticket not created!" - click.echo("Initialized Kerberos TGT using machine keytab") - from requests_kerberos import HTTPKerberosAuth, OPTIONAL - auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) - else: - click.echo("Not using machine keytab") - auth = None - - 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" - } - - if renew and cert: - signer = key.signer( - padding.PSS( - mgf=padding.MGF1(hashes.SHA512()), - salt_length=padding.PSS.MAX_LENGTH - ), - hashes.SHA512() - ) - signer.update(cert_buf) - signer.update(request_buf) - headers["X-Renewal-Signature"] = b64encode(signer.finalize()) - click.echo("Attached renewal signature %s" % headers["X-Renewal-Signature"]) - - submission = requests.post(request_url, auth=auth, data=open(request_path), headers=headers) - - # 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) - return - 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 - else: - submission.raise_for_status() - - try: - cert = x509.load_pem_x509_certificate(submission.text.encode("ascii"), default_backend()) - except: # TODO: catch correct exceptions - raise ValueError("Failed to parse PEM: %s" % submission.text) - - os.umask(0o022) - certificate_partial = tempfile.mktemp(prefix=certificate_path + ".part") - with open(certificate_partial, "w") as fh: - # Dump certificate - fh.write(submission.text) - - # Bundle CA certificate, necessary for nginx - if bundle: - with open(authority_path) as ch: - fh.write(ch.read()) - - click.echo("Writing certificate to: %s" % certificate_path) - selinux_fixup(certificate_partial) - os.rename(certificate_partial, certificate_path) - - # TODO: Validate fetched certificate against CA - # TODO: Check that recevied certificate CN and pubkey match - # TODO: Check file permissions