mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
cli: Migrate client side to oscrypto
This commit is contained in:
parent
5d48abe973
commit
61aa54695e
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
|
|
Loading…
Reference in New Issue
Block a user