mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 09:29:13 +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 string | ||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
|  | from base64 import b64encode | ||||||
| from configparser import ConfigParser, NoOptionError, NoSectionError | 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, selinux_fixup | ||||||
| from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges |  | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from time import sleep | from time import sleep | ||||||
| import const | import const | ||||||
| @@ -93,11 +93,15 @@ def setup_client(prefix="client_", dh=False): | |||||||
| def certidude_request(fork, renew, no_wait, system_keytab_required): | def certidude_request(fork, renew, no_wait, system_keytab_required): | ||||||
|     # Here let's try to avoid compiling packages from scratch |     # Here let's try to avoid compiling packages from scratch | ||||||
|     rpm("openssl") or \ |     rpm("openssl") or \ | ||||||
|     apt("openssl python-cryptography python-jinja2") or \ |     apt("openssl python-jinja2") or \ | ||||||
|     pip("cryptography jinja2") |     pip("jinja2 oscrypto csrbuilder asn1crypto") | ||||||
|  |  | ||||||
|     import requests |     import requests | ||||||
|     from jinja2 import Environment, PackageLoader |     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) |     env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) | ||||||
|  |  | ||||||
|     if not os.path.exists(const.CLIENT_CONFIG_PATH): |     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)) |             fh.write(env.get_template("client/certidude.service").render(context)) | ||||||
|  |  | ||||||
|  |  | ||||||
|     for authority in clients.sections(): |     for authority_name 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 |  | ||||||
|         # TODO: Create directories automatically |         # TODO: Create directories automatically | ||||||
|  |  | ||||||
|         if clients.get(authority, "trigger") == "domain joined": |         try: | ||||||
|             system_keytab_required = True |             trigger = clients.get(authority_name, "trigger") | ||||||
|         elif clients.get(authority, "trigger") != "interface up": |         except NoOptionError: | ||||||
|             continue |             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 |             # Stop further processing if command line argument said so or trigger expects domain membership | ||||||
|             if not os.path.exists("/etc/krb5.keytab"): |             if not os.path.exists("/etc/krb5.keytab"): | ||||||
|                 continue |                 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: |         try: | ||||||
|             with open(pid_path) as fh: |             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)) |             click.echo("Spawned certificate request process with PID %d" % (child_pid)) | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|  |  | ||||||
|         with open(pid_path, "w") as fh: |         with open(pid_path, "w") as fh: | ||||||
|             fh.write("%d\n" % os.getpid()) |             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: |             try: | ||||||
|                 certidude_request_certificate( |                 cert = asymmetric.load_certificate(submission.content) | ||||||
|                     authority, |             except: # TODO: catch correct exceptions | ||||||
|                     system_keytab_required, |                 raise ValueError("Failed to parse PEM: %s" % submission.text) | ||||||
|                     endpoint_key_path, |  | ||||||
|                     endpoint_request_path, |             os.umask(0o022) | ||||||
|                     endpoint_certificate_path, |             certificate_partial = certificate_path + ".part" | ||||||
|                     endpoint_authority_path, |             with open(certificate_partial, "w") as fh: | ||||||
|                     endpoint_revocations_path, |                 # Dump certificate | ||||||
|                     endpoint_common_name, |                 fh.write(submission.text) | ||||||
|                     endpoint_renewal_overlap, |  | ||||||
|                     insecure=endpoint_insecure, |             click.echo("Writing certificate to: %s" % certificate_path) | ||||||
|                     autosign=True, |             selinux_fixup(certificate_partial) | ||||||
|                     wait=not no_wait, |             os.rename(certificate_partial, certificate_path) | ||||||
|                     renew=renew) |  | ||||||
|                 break |             # Nginx requires bundle | ||||||
|             except requests.exceptions.Timeout: |             try: | ||||||
|                 retries -= 1 |                 bundle_path = clients.get(authority_name, "bundle path") | ||||||
|                 continue |             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(): |         for endpoint in service_config.sections(): | ||||||
|             if service_config.get(endpoint, "authority") != authority: |             if service_config.get(endpoint, "authority") != authority_name: | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             click.echo("Configuring '%s'" % endpoint) |             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 |                     # Identify correct ipsec.conf section by leftcert | ||||||
|                     if section_type != "conn": |                     if section_type != "conn": | ||||||
|                         continue |                         continue | ||||||
|                     if config[section_type,section_name]["leftcert"] != endpoint_certificate_path: |                     if config[section_type,section_name]["leftcert"] != certificate_path: | ||||||
|                         continue |                         continue | ||||||
|  |  | ||||||
|                     if config[section_type,section_name].get("left", "") == "%defaultroute": |                     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 |             # OpenVPN set up with NetworkManager | ||||||
|             if service_config.get(endpoint, "service") == "network-manager/openvpn": |             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 |                 # NetworkManager-strongswan-gnome | ||||||
|                 nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint) |                 nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint) | ||||||
|                 if os.path.exists(nm_config_path): |                 if os.path.exists(nm_config_path): | ||||||
| @@ -300,6 +479,7 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): | |||||||
|                     continue |                     continue | ||||||
|                 nm_config = ConfigParser() |                 nm_config = ConfigParser() | ||||||
|                 nm_config.add_section("connection") |                 nm_config.add_section("connection") | ||||||
|  |                 nm_config.set("connection", "certidude managed", "true") | ||||||
|                 nm_config.set("connection", "id", endpoint) |                 nm_config.set("connection", "id", endpoint) | ||||||
|                 nm_config.set("connection", "uuid", uuid) |                 nm_config.set("connection", "uuid", uuid) | ||||||
|                 nm_config.set("connection", "type", "vpn") |                 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", "tap-dev", "no") | ||||||
|                 nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate |                 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", "remote", service_config.get(endpoint, "remote")) | ||||||
|                 nm_config.set("vpn", "port", str(endpoint_port)) |                 nm_config.set("vpn", "key", key_path) | ||||||
|                 if endpoint_proto == "tcp": |                 nm_config.set("vpn", "cert", certificate_path) | ||||||
|                     nm_config.set("vpn", "proto-tcp", "yes") |                 nm_config.set("vpn", "ca", authority_path) | ||||||
|                 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.add_section("ipv4") |                 nm_config.add_section("ipv4") | ||||||
|                 nm_config.set("ipv4", "method", "auto") |                 nm_config.set("ipv4", "method", "auto") | ||||||
|                 nm_config.set("ipv4", "never-default", "true") |                 nm_config.set("ipv4", "never-default", "true") | ||||||
|                 nm_config.add_section("ipv6") |                 nm_config.add_section("ipv6") | ||||||
|                 nm_config.set("ipv6", "method", "auto") |                 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 |                 # Prevent creation of files with liberal permissions | ||||||
|                 os.umask(0o177) |                 os.umask(0o177) | ||||||
|  |  | ||||||
| @@ -336,10 +524,11 @@ def certidude_request(fork, renew, no_wait, system_keytab_required): | |||||||
|  |  | ||||||
|  |  | ||||||
|             # IPSec set up with NetworkManager |             # 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() |                 client_config = ConfigParser() | ||||||
|                 nm_config = ConfigParser() |                 nm_config = ConfigParser() | ||||||
|                 nm_config.add_section("connection") |                 nm_config.add_section("connection") | ||||||
|  |                 nm_config.set("connection", "certidude managed", "true") | ||||||
|                 nm_config.set("connection", "id", endpoint) |                 nm_config.set("connection", "id", endpoint) | ||||||
|                 nm_config.set("connection", "uuid", uuid) |                 nm_config.set("connection", "uuid", uuid) | ||||||
|                 nm_config.set("connection", "type", "vpn") |                 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", "method", "key") | ||||||
|                 nm_config.set("vpn", "ipcomp", "no") |                 nm_config.set("vpn", "ipcomp", "no") | ||||||
|                 nm_config.set("vpn", "address", service_config.get(endpoint, "remote")) |                 nm_config.set("vpn", "address", service_config.get(endpoint, "remote")) | ||||||
|                 nm_config.set("vpn", "userkey", endpoint_key_path) |                 nm_config.set("vpn", "userkey", key_path) | ||||||
|                 nm_config.set("vpn", "usercert", endpoint_certificate_path) |                 nm_config.set("vpn", "usercert", certificate_path) | ||||||
|                 nm_config.set("vpn", "certificate", endpoint_authority_path) |                 nm_config.set("vpn", "certificate", authority_path) | ||||||
|                 nm_config.add_section("ipv4") |                 nm_config.add_section("ipv4") | ||||||
|                 nm_config.set("ipv4", "method", "auto") |                 nm_config.set("ipv4", "method", "auto") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,15 @@ import os | |||||||
| import click | import click | ||||||
| import subprocess | 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(): | def drop_privileges(): | ||||||
|     from certidude import config |     from certidude import config | ||||||
|     import pwd |     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") |     click.echo("Failed to resolve fully qualified hostname of this machine, make sure hostname -f works") | ||||||
|     sys.exit(254) |     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 | # TODO: lazier, otherwise gets evaluated before installing package | ||||||
| if os.path.exists("/etc/strongswan/ipsec.conf"): # fedora dafuq?! | 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