certidude/certidude/helpers.py

228 lines
9.6 KiB
Python

import click
import os
import requests
import subprocess
import tempfile
from certidude import errors
from certidude.wrappers import Certificate, Request
from configparser import ConfigParser
from OpenSSL import crypto
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
# 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
authority_url = "http://%s/api/certificate/" % server
request_url = "http://%s/api/request/" % server
revoked_url = "http://%s/api/revoked/" % server
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"})
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
except crypto.Error:
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.text)
click.echo("Writing authority certificate to: %s" % authority_path)
os.rename(authority_partial, authority_path)
# Fetch certificate revocation list
r = requests.get(revoked_url, stream=True)
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")
os.rename(revocations_partial, revocations_path)
# Check if we have been inserted into CRL
if os.path.exists(certificate_path):
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read())
revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read())
for revocation in revocation_list.get_revoked():
if int(revocation.get_serial(), 16) == cert.get_serial_number():
if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL'
# TODO: Disable service for time being
click.echo("Certificate put on hold, doing nothing for now")
break
# Disable the client if operation has been ceased or
# the certificate has been superseded by other
if revocation.get_reason() in ("Cessation Of Operation", "Superseded"):
if os.path.exists("/etc/certidude/client.conf"):
clients.readfp(open("/etc/certidude/client.conf"))
if clients.has_section(server):
clients.set(server, "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
if revocation.get_reason() in ("CA Compromise", "AA Compromise"):
if os.path.exists(authority_path):
os.remove(key_path)
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 = Request(open(request_path))
click.echo("Found signing request: %s" % request_path)
except EnvironmentError:
# Construct private key
click.echo("Generating 4096-bit RSA key...")
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 4096)
# Dump private key
key_partial = tempfile.mktemp(prefix=key_path + ".part")
os.umask(0o077)
with open(key_partial, "wb") as fh:
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
# Construct CSR
csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key)
csr.get_subject().CN = common_name
request = Request(csr)
# Set subject attributes
if given_name:
request.given_name = given_name
if surname:
request.surname = surname
if org_unit:
request.organizational_unit = org_unit
# Collect subject alternative names
subject_alt_name = set()
if email_address:
subject_alt_name.add("email:%s" % email_address)
if ip_address:
subject_alt_name.add("IP:%s" % ip_address)
if dns:
subject_alt_name.add("DNS:%s" % dns)
# Set extensions
extensions = []
if key_usage:
extensions.append(("keyUsage", key_usage, True))
if extended_key_usage:
extensions.append(("extendedKeyUsage", extended_key_usage, False))
if subject_alt_name:
extensions.append(("subjectAltName", ", ".join(subject_alt_name), False))
request.set_extensions(extensions)
# Dump CSR
os.umask(0o022)
with open(request_path + ".part", "w") as fh:
fh.write(request.dump())
click.echo("Writing private key to: %s" % key_path)
os.rename(key_partial, key_path)
click.echo("Writing certificate signing request to: %s" % request_path)
os.rename(request_path + ".part", request_path)
# We have CSR now, save the paths to client.conf so we could:
# Update CRL, renew certificate, maybe something extra?
if not os.path.exists("/etc/certidude"):
os.makedirs("/etc/certidude")
clients = ConfigParser()
if os.path.exists("/etc/certidude/client.conf"):
clients.readfp(open("/etc/certidude/client.conf"))
if clients.has_section(server):
click.echo("Section %s already exists in /etc/certidude/client.conf, not reconfiguring" % server)
else:
clients.add_section(server)
clients.set(server, "trigger", "interface up")
clients.set(server, "key_path", key_path)
clients.set(server, "request_path", request_path)
clients.set(server, "certificate_path", certificate_path)
clients.set(server, "authority_path", authority_path)
clients.set(server, "key_path", key_path)
clients.set(server, "revocations_path", revocations_path)
clients.write(open("/etc/certidude/client.conf", "w"))
click.echo("Section %s added to /etc/certidude/client.conf" % repr(server))
if os.path.exists(certificate_path):
click.echo("Found certificate: %s" % certificate_path)
# TODO: Check certificate validity, download CRL?
return
click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url,
data=open(request_path),
headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"})
if submission.status_code == requests.codes.ok:
pass
if submission.status_code == requests.codes.accepted:
# Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now
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 = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % submission.text)
os.umask(0o022)
with open(certificate_path + ".part", "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)
os.rename(certificate_path + ".part", certificate_path)
# TODO: Validate fetched certificate against CA
# TODO: Check that recevied certificate CN and pubkey match
# TODO: Check file permissions