mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 17:39:12 +00:00 
			
		
		
		
	cli: Migrate client side to oscrypto
This commit is contained in:
		
							
								
								
									
										357
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						
									
										357
									
								
								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") | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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?! | ||||
|   | ||||
| @@ -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 | ||||
		Reference in New Issue
	
	Block a user