mirror of
https://github.com/laurivosandi/certidude
synced 2024-09-28 21:11:42 +00:00
Lauri Võsandi
925bc0ef9a
* Add abstraction for user objects * Mail authority admins about pending, revoked and signed certificates * Add NetworkManager's OpenVPN plugin support * Improve CRL support * Refactor CSRF protection * Update documentation
228 lines
9.6 KiB
Python
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
|