cli: Migrate client side to oscrypto

This commit is contained in:
Lauri Võsandi 2017-05-27 21:17:21 +03:00
parent 5d48abe973
commit 61aa54695e
4 changed files with 287 additions and 357 deletions

View File

@ -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")

View File

@ -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

View File

@ -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?!

View File

@ -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