1
0
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:
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 subprocess
import sys
from base64 import b64encode
from configparser import ConfigParser, NoOptionError, NoSectionError
from certidude.helpers import certidude_request_certificate
from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges
from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup
from datetime import datetime, timedelta
from time import sleep
import const
@ -93,11 +93,15 @@ def setup_client(prefix="client_", dh=False):
def certidude_request(fork, renew, no_wait, system_keytab_required):
# Here let's try to avoid compiling packages from scratch
rpm("openssl") or \
apt("openssl python-cryptography python-jinja2") or \
pip("cryptography jinja2")
apt("openssl python-jinja2") or \
pip("jinja2 oscrypto csrbuilder asn1crypto")
import requests
from jinja2 import Environment, PackageLoader
from oscrypto import asymmetric
from asn1crypto import crl, pem
from csrbuilder import CSRBuilder, pem_armor_csr
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
if not os.path.exists(const.CLIENT_CONFIG_PATH):
@ -129,52 +133,30 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
fh.write(env.get_template("client/certidude.service").render(context))
for authority in clients.sections():
try:
endpoint_renewal_overlap = clients.getint(authority, "renewal overlap")
except NoOptionError:
endpoint_renewal_overlap = None
try:
endpoint_insecure = clients.getboolean(authority, "insecure")
except NoOptionError:
endpoint_insecure = False
try:
endpoint_common_name = clients.get(authority, "common name")
except NoOptionError:
endpoint_common_name = const.HOSTNAME
try:
endpoint_key_path = clients.get(authority, "key path")
except NoOptionError:
endpoint_key_path = "/var/lib/certidude/%s/keys/%s.pem" % (authority, const.HOSTNAME)
try:
endpoint_request_path = clients.get(authority, "request path")
except NoOptionError:
endpoint_request_path = "/var/lib/certidude/%s/requests/%s.pem" % (authority, const.HOSTNAME)
try:
endpoint_certificate_path = clients.get(authority, "certificate path")
except NoOptionError:
endpoint_certificate_path = "/var/lib/certidude/%s/signed/%s.pem" % (authority, const.HOSTNAME)
try:
endpoint_authority_path = clients.get(authority, "authority path")
except NoOptionError:
endpoint_authority_path = "/var/lib/certidude/%s/ca_crt.pem" % authority
try:
endpoint_revocations_path = clients.get(authority, "revocations path")
except NoOptionError:
endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority
for authority_name in clients.sections():
# TODO: Create directories automatically
if clients.get(authority, "trigger") == "domain joined":
system_keytab_required = True
elif clients.get(authority, "trigger") != "interface up":
continue
try:
trigger = clients.get(authority_name, "trigger")
except NoOptionError:
trigger = "interface up"
if system_keytab_required:
if trigger == "domain joined":
# Stop further processing if command line argument said so or trigger expects domain membership
if not os.path.exists("/etc/krb5.keytab"):
continue
use_keytab = True
elif trigger == "interface up":
pass
else:
raise
pid_path = os.path.join(const.RUN_DIR, authority + ".pid")
#########################
### Fork if requested ###
#########################
pid_path = os.path.join(const.RUN_DIR, authority_name + ".pid")
try:
with open(pid_path) as fh:
@ -194,34 +176,239 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
click.echo("Spawned certificate request process with PID %d" % (child_pid))
continue
with open(pid_path, "w") as fh:
fh.write("%d\n" % os.getpid())
retries = 30
while retries > 0:
try:
scheme = "http" if clients.getboolean(authority_name, "insecure") else "https"
except NoOptionError:
scheme = "https"
# Expand ca.example.com
authority_url = "%s://%s/api/certificate/" % (scheme, authority_name)
request_url = "%s://%s/api/request/" % (scheme, authority_name)
revoked_url = "%s://%s/api/revoked/" % (scheme, authority_name)
try:
authority_path = clients.get(authority_name, "authority path")
except NoOptionError:
authority_path = "/var/lib/certidude/%s/ca_crt.pem" % authority_name
finally:
if os.path.exists(authority_path):
click.echo("Found authority certificate in: %s" % authority_path)
else:
if not os.path.exists(os.path.dirname(authority_path)):
os.makedirs(os.path.dirname(authority_path))
click.echo("Attempting to fetch authority certificate from %s" % authority_url)
try:
r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
asymmetric.load_certificate(r.content)
except:
raise
# raise ValueError("Failed to parse PEM: %s" % r.text)
authority_partial = authority_path + ".part"
with open(authority_partial, "w") as oh:
oh.write(r.content)
click.echo("Writing authority certificate to: %s" % authority_path)
selinux_fixup(authority_partial)
os.rename(authority_partial, authority_path)
# Attempt to install CA certificates system wide
try:
authority_system_wide = clients.getboolean(authority_name, "system wide")
except NoOptionError:
authority_system_wide = False
finally:
if authority_system_wide:
# Firefox, Chromium, wget, curl on Fedora
# Note that if ~/.pki/nssdb has been customized before, curl breaks
if os.path.exists("/usr/bin/update-ca-trust"):
link_path = "/etc/pki/ca-trust/source/anchors/%s" % authority_name
if not os.path.lexists(link_path):
os.symlink(authority_path, link_path)
os.system("update-ca-trust")
# curl on Fedora ?
# pip
###############
### Get CRL ###
###############
try:
revocations_path = clients.get(authority_name, "revocations path")
except NoOptionError:
revocations_path = None
else:
# Fetch certificate revocation list
click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path))
r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})
assert r.status_code == 200, "Failed to fetch CRL from %s, got %s" % (revoked_url, r.text)
#revocations = crl.CertificateList.load(pem.unarmor(r.content))
# TODO: check signature, parse reasons, remove keys if revoked
revocations_partial = revocations_path + ".part"
with open(revocations_partial, 'wb') as f:
f.write(r.content)
try:
common_name = clients.get(authority_name, "common name")
except NoOptionError:
click.echo("No common name specified for %s, not requesting a certificate" % authority_name)
continue
################################
### Generate keypair and CSR ###
################################
try:
key_path = clients.get(authority_name, "key path")
request_path = clients.get(authority_name, "request path")
except NoOptionError:
key_path = "/var/lib/certidude/%s/client_key.pem" % authority_name
request_path = "/var/lib/certidude/%s/client_csr.pem" % authority_name
if not os.path.exists(request_path):
key_partial = key_path + ".part"
request_partial = request_path + ".part"
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048)
builder = CSRBuilder({u"common_name": common_name}, public_key)
request = builder.build(private_key)
with open(key_partial, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None))
with open(request_partial, 'wb') as f:
f.write(pem_armor_csr(request))
selinux_fixup(key_partial)
selinux_fixup(request_partial)
os.rename(key_partial, key_path)
os.rename(request_partial, request_path)
##############################################
### Submit CSR and save signed certificate ###
##############################################
try:
certificate_path = clients.get(authority_name, "certificate path")
except NoOptionError:
certificate_path = "/var/lib/certidude/%s/client_cert.pem" % authority_name
headers={
"Content-Type": "application/pkcs10",
"Accept": "application/x-x509-user-cert,application/x-pem-file"
}
try:
# Attach renewal signature if renewal requested and cert exists
renewal_overlap = clients.getint(authority_name, "renewal overlap")
with open(certificate_path) as ch, open(request_path) as rh, open(key_path) as kh:
cert_buf = ch.read()
cert = asymmetric.load_certificate(cert_buf)
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native
if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap):
click.echo("Certificate will expire %s, will attempt to renew" % expires)
renew = True
headers["X-Renewal-Signature"] = b64encode(
asymmetric.rsa_pss_sign(
asymmetric.load_private_key(kh.read()),
cert_buf + rh.read(),
"sha512"))
except NoOptionError: # Renewal not specified in config
pass
except EnvironmentError: # Certificate missing
pass
else:
click.echo("Attached renewal signature %s" % headers["X-Renewal-Signature"])
if not os.path.exists(certificate_path) or renew:
# Set up URL-s
request_params = set()
request_params.add("autosign=true")
if not no_wait:
request_params.add("wait=forever")
if request_params:
request_url = request_url + "?" + "&".join(request_params)
# If machine is joined to domain attempt to present machine credentials for authentication
if use_keytab:
os.environ["KRB5CCNAME"]="/tmp/ca.ticket"
# Mac OS X has keytab with lowercase hostname
cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.lower())
click.echo("Executing: %s" % cmd)
if os.system(cmd):
# Fedora /w SSSD has keytab with uppercase hostname
cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.upper())
if os.system(cmd):
# Failed, probably /etc/krb5.keytab contains spaghetti
raise ValueError("Failed to initialize TGT using machine keytab")
assert os.path.exists("/tmp/ca.ticket"), "Ticket not created!"
click.echo("Initialized Kerberos TGT using machine keytab")
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
else:
click.echo("Not using machine keytab")
auth = None
submission = requests.post(request_url, auth=auth, data=open(request_path), headers=headers)
# Destroy service ticket
if os.path.exists("/tmp/ca.ticket"):
os.system("kdestroy")
if submission.status_code == requests.codes.ok:
pass
if submission.status_code == requests.codes.accepted:
click.echo("Server accepted the request, but refused to sign immideately (%s). Waiting was not requested, hence quitting for now" % submission.text)
return
if submission.status_code == requests.codes.conflict:
raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite")
elif submission.status_code == requests.codes.gone:
# Should the client retry or disable request submission?
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
else:
submission.raise_for_status()
try:
certidude_request_certificate(
authority,
system_keytab_required,
endpoint_key_path,
endpoint_request_path,
endpoint_certificate_path,
endpoint_authority_path,
endpoint_revocations_path,
endpoint_common_name,
endpoint_renewal_overlap,
insecure=endpoint_insecure,
autosign=True,
wait=not no_wait,
renew=renew)
break
except requests.exceptions.Timeout:
retries -= 1
continue
cert = asymmetric.load_certificate(submission.content)
except: # TODO: catch correct exceptions
raise ValueError("Failed to parse PEM: %s" % submission.text)
os.umask(0o022)
certificate_partial = certificate_path + ".part"
with open(certificate_partial, "w") as fh:
# Dump certificate
fh.write(submission.text)
click.echo("Writing certificate to: %s" % certificate_path)
selinux_fixup(certificate_partial)
os.rename(certificate_partial, certificate_path)
# Nginx requires bundle
try:
bundle_path = clients.get(authority_name, "bundle path")
except NoOptionError:
pass
else:
bundle_partial = bundle_path + ".part"
with open(bundle_partial, "w") as fh:
fh.write(submission.text)
with open(authority_path) as ch:
fh.write(ch.read())
click.echo("Writing bundle to: %s" % bundle_path)
os.rename(bundle_partial, bundle_path)
##################################
### Configure related services ###
##################################
for endpoint in service_config.sections():
if service_config.get(endpoint, "authority") != authority:
if service_config.get(endpoint, "authority") != authority_name:
continue
click.echo("Configuring '%s'" % endpoint)
@ -256,7 +443,7 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
# Identify correct ipsec.conf section by leftcert
if section_type != "conn":
continue
if config[section_type,section_name]["leftcert"] != endpoint_certificate_path:
if config[section_type,section_name]["leftcert"] != certificate_path:
continue
if config[section_type,section_name].get("left", "") == "%defaultroute":
@ -285,14 +472,6 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
# OpenVPN set up with NetworkManager
if service_config.get(endpoint, "service") == "network-manager/openvpn":
try:
endpoint_port = service_config.getint(endpoint, "port")
except NoOptionError:
endpoint_port = 1194
try:
endpoint_proto = service_config.get(endpoint, "proto")
except NoOptionError:
endpoint_proto = "udp"
# NetworkManager-strongswan-gnome
nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint)
if os.path.exists(nm_config_path):
@ -300,6 +479,7 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
continue
nm_config = ConfigParser()
nm_config.add_section("connection")
nm_config.set("connection", "certidude managed", "true")
nm_config.set("connection", "id", endpoint)
nm_config.set("connection", "uuid", uuid)
nm_config.set("connection", "type", "vpn")
@ -311,18 +491,26 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
nm_config.set("vpn", "tap-dev", "no")
nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
nm_config.set("vpn", "remote", service_config.get(endpoint, "remote"))
nm_config.set("vpn", "port", str(endpoint_port))
if endpoint_proto == "tcp":
nm_config.set("vpn", "proto-tcp", "yes")
nm_config.set("vpn", "key", endpoint_key_path)
nm_config.set("vpn", "cert", endpoint_certificate_path)
nm_config.set("vpn", "ca", endpoint_authority_path)
nm_config.set("vpn", "key", key_path)
nm_config.set("vpn", "cert", certificate_path)
nm_config.set("vpn", "ca", authority_path)
nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto")
nm_config.set("ipv4", "never-default", "true")
nm_config.add_section("ipv6")
nm_config.set("ipv6", "method", "auto")
try:
nm_config.set("vpn", "port", str(service_config.getint(endpoint, "port")))
except NoOptionError:
nm_config.set("vpn", "port", "1194")
try:
if service_config.get(endpoint, "proto") == "tcp":
nm_config.set("vpn", "proto-tcp", "yes")
except NoOptionError:
pass
# Prevent creation of files with liberal permissions
os.umask(0o177)
@ -336,10 +524,11 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
# IPSec set up with NetworkManager
elif service_config.get(endpoint, "service") == "network-manager/strongswan":
if service_config.get(endpoint, "service") == "network-manager/strongswan":
client_config = ConfigParser()
nm_config = ConfigParser()
nm_config.add_section("connection")
nm_config.set("connection", "certidude managed", "true")
nm_config.set("connection", "id", endpoint)
nm_config.set("connection", "uuid", uuid)
nm_config.set("connection", "type", "vpn")
@ -350,9 +539,9 @@ def certidude_request(fork, renew, no_wait, system_keytab_required):
nm_config.set("vpn", "method", "key")
nm_config.set("vpn", "ipcomp", "no")
nm_config.set("vpn", "address", service_config.get(endpoint, "remote"))
nm_config.set("vpn", "userkey", endpoint_key_path)
nm_config.set("vpn", "usercert", endpoint_certificate_path)
nm_config.set("vpn", "certificate", endpoint_authority_path)
nm_config.set("vpn", "userkey", key_path)
nm_config.set("vpn", "usercert", certificate_path)
nm_config.set("vpn", "certificate", authority_path)
nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto")

View File

@ -3,6 +3,15 @@ import os
import click
import subprocess
def selinux_fixup(path):
"""
Fix OpenVPN credential store security context on Fedora
"""
if not os.path.exists("/sys/fs/selinux"):
return
cmd = "chcon", "--type=home_cert_t", path
subprocess.call(cmd)
def drop_privileges():
from certidude import config
import pwd

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")
sys.exit(254)
HOSTNAME, DOMAIN = FQDN.split(".", 1)
try:
HOSTNAME, DOMAIN = FQDN.split(".", 1)
except ValueError: # If FQDN is not configured
HOSTNAME = FQDN
DOMAIN = None
# TODO: lazier, otherwise gets evaluated before installing package
if os.path.exists("/etc/strongswan/ipsec.conf"): # fedora dafuq?!

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