mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +00:00
Refactor signature request submission
Certidude client now reads configuration from /etc/certidude/client.conf, submits CSR-s and once signed configures services based on /etc/certidude/services.conf
This commit is contained in:
parent
d8abde3d53
commit
f2df17bb88
@ -8,6 +8,7 @@ from OpenSSL import crypto
|
|||||||
from certidude import config, push
|
from certidude import config, push
|
||||||
from certidude.wrappers import Certificate, Request
|
from certidude.wrappers import Certificate, Request
|
||||||
from certidude.signer import raw_sign
|
from certidude.signer import raw_sign
|
||||||
|
from certidude import errors
|
||||||
|
|
||||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
|
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
|
||||||
|
|
||||||
@ -15,12 +16,6 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0
|
|||||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||||
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
||||||
|
|
||||||
class RequestExists(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class DuplicateCommonNameError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def publish_certificate(func):
|
def publish_certificate(func):
|
||||||
# TODO: Implement e-mail and nginx notifications using hooks
|
# TODO: Implement e-mail and nginx notifications using hooks
|
||||||
def wrapped(csr, *args, **kwargs):
|
def wrapped(csr, *args, **kwargs):
|
||||||
@ -68,9 +63,9 @@ def store_request(buf, overwrite=False):
|
|||||||
# If there is cert, check if it's the same
|
# If there is cert, check if it's the same
|
||||||
if os.path.exists(request_path):
|
if os.path.exists(request_path):
|
||||||
if open(request_path).read() == buf:
|
if open(request_path).read() == buf:
|
||||||
raise RequestExists("Request already exists")
|
raise errors.RequestExists("Request already exists")
|
||||||
else:
|
else:
|
||||||
raise DuplicateCommonNameError("Another request with same common name already exists")
|
raise errors.DuplicateCommonNameError("Another request with same common name already exists")
|
||||||
else:
|
else:
|
||||||
with open(request_path + ".part", "w") as fh:
|
with open(request_path + ".part", "w") as fh:
|
||||||
fh.write(buf)
|
fh.write(buf)
|
||||||
|
207
certidude/cli.py
207
certidude/cli.py
@ -9,6 +9,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -23,6 +24,7 @@ from time import sleep
|
|||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
|
|
||||||
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
|
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
|
||||||
|
|
||||||
# Big fat warning:
|
# Big fat warning:
|
||||||
@ -60,12 +62,162 @@ if os.getuid() >= 1000:
|
|||||||
FIRST_NAME = gecos
|
FIRST_NAME = gecos
|
||||||
|
|
||||||
|
|
||||||
@click.command("spawn", help="Run privilege isolated signer process")
|
@click.command("request", help="Run processes for requesting certificates and configuring services")
|
||||||
|
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
|
||||||
|
def certidude_spawn_request(fork):
|
||||||
|
from certidude.helpers import certidude_request_certificate
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
clients = ConfigParser()
|
||||||
|
clients.readfp(open("/etc/certidude/client.conf"))
|
||||||
|
|
||||||
|
services = ConfigParser()
|
||||||
|
services.readfp(open("/etc/certidude/services.conf"))
|
||||||
|
|
||||||
|
# Process directories
|
||||||
|
run_dir = "/run/certidude"
|
||||||
|
|
||||||
|
# Prepare signer PID-s directory
|
||||||
|
if not os.path.exists(run_dir):
|
||||||
|
click.echo("Creating: %s" % run_dir)
|
||||||
|
os.makedirs(run_dir)
|
||||||
|
|
||||||
|
for certificate in clients.sections():
|
||||||
|
if clients.get(certificate, "managed") != "true":
|
||||||
|
continue
|
||||||
|
|
||||||
|
pid_path = os.path.join(run_dir, certificate + ".pid")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(pid_path) as fh:
|
||||||
|
pid = int(fh.readline())
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
click.echo("Terminated process %d" % pid)
|
||||||
|
os.unlink(pid_path)
|
||||||
|
except (ValueError, ProcessLookupError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if fork:
|
||||||
|
child_pid = os.fork()
|
||||||
|
else:
|
||||||
|
child_pid = None
|
||||||
|
|
||||||
|
if child_pid:
|
||||||
|
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())
|
||||||
|
setproctitle("certidude spawn request %s" % certificate)
|
||||||
|
retries = 30
|
||||||
|
while retries > 0:
|
||||||
|
try:
|
||||||
|
certidude_request_certificate(
|
||||||
|
clients.get(certificate, "server"),
|
||||||
|
clients.get(certificate, "key_path"),
|
||||||
|
clients.get(certificate, "request_path"),
|
||||||
|
clients.get(certificate, "certificate_path"),
|
||||||
|
clients.get(certificate, "authority_path"),
|
||||||
|
socket.gethostname(),
|
||||||
|
None,
|
||||||
|
autosign=True,
|
||||||
|
wait=True)
|
||||||
|
break
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
retries -= 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
for endpoint in services.sections():
|
||||||
|
if services.get(endpoint, "certificate") != certificate:
|
||||||
|
continue
|
||||||
|
|
||||||
|
csummer = hashlib.sha1()
|
||||||
|
csummer.update(endpoint.encode("ascii"))
|
||||||
|
csum = csummer.hexdigest()
|
||||||
|
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
|
||||||
|
|
||||||
|
# Set up IPsec via NetworkManager
|
||||||
|
if services.get(endpoint, "service") == "network-manager/strongswan":
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.add_section("connection")
|
||||||
|
config.add_section("vpn")
|
||||||
|
config.add_section("ipv4")
|
||||||
|
|
||||||
|
config.set("connection", "id", endpoint)
|
||||||
|
config.set("connection", "uuid", uuid)
|
||||||
|
config.set("connection", "type", "vpn")
|
||||||
|
|
||||||
|
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
|
||||||
|
config.set("vpn", "userkey", clients.get(certificate, "key_path"))
|
||||||
|
config.set("vpn", "usercert", clients.get(certificate, "certificate_path"))
|
||||||
|
config.set("vpn", "encap", "no")
|
||||||
|
config.set("vpn", "address", services.get(endpoint, "remote"))
|
||||||
|
config.set("vpn", "virtual", "yes")
|
||||||
|
config.set("vpn", "method", "key")
|
||||||
|
config.set("vpn", "certificate", clients.get(certificate, "authority_path"))
|
||||||
|
config.set("vpn", "ipcomp", "no")
|
||||||
|
|
||||||
|
config.set("ipv4", "method", "auto")
|
||||||
|
|
||||||
|
# Add routes, may need some more tweaking
|
||||||
|
for index, subnet in enumerate(services.get(endpoint, "route").split(","), start=1):
|
||||||
|
config.set("ipv4", "route%d" % index, subnet)
|
||||||
|
|
||||||
|
# Prevent creation of files with liberal permissions
|
||||||
|
os.umask(0o177)
|
||||||
|
|
||||||
|
# Write keyfile
|
||||||
|
with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as configfile:
|
||||||
|
config.write(configfile)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Set up IPsec via /etc/ipsec.conf
|
||||||
|
if services.get(endpoint, "service") == "strongswan":
|
||||||
|
from ipsecparse import loads
|
||||||
|
config = loads(open('/etc/ipsec.conf').read())
|
||||||
|
config["conn", endpoint] = dict(
|
||||||
|
leftsourceip="%config",
|
||||||
|
left="%defaultroute",
|
||||||
|
leftcert=clients.get(certificate, "certificate_path"),
|
||||||
|
rightid="%any",
|
||||||
|
right=services.get(endpoint, "remote"),
|
||||||
|
rightsubnet=services.get(endpoint, "route"),
|
||||||
|
keyexchange="ikev2",
|
||||||
|
keyingtries="300",
|
||||||
|
dpdaction="restart",
|
||||||
|
closeaction="restart",
|
||||||
|
auto="start")
|
||||||
|
with open("/etc/ipsec.conf.part", "w") as fh:
|
||||||
|
fh.write(config.dumps())
|
||||||
|
os.rename("/etc/ipsec.conf.part", "/etc/ipsec.conf")
|
||||||
|
|
||||||
|
# Regenerate /etc/ipsec.secrets
|
||||||
|
with open("/etc/ipsec.secrets.part", "w") as fh:
|
||||||
|
for filename in os.listdir("/etc/ipsec.d/private"):
|
||||||
|
if not filename.endswith(".pem"):
|
||||||
|
continue
|
||||||
|
fh.write(": RSA /etc/ipsec.d/private/%s\n" % filename)
|
||||||
|
os.rename("/etc/ipsec.secrets.part", "/etc/ipsec.secrets")
|
||||||
|
|
||||||
|
# Attempt to reload config or start if it's not running
|
||||||
|
if os.system("ipsec update") == 130:
|
||||||
|
os.system("ipsec start")
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS, <insert awesomeness here>
|
||||||
|
|
||||||
|
os.unlink(pid_path)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("signer", help="Run privilege isolated signer process")
|
||||||
@click.option("-k", "--kill", default=False, is_flag=True, help="Kill previous instance")
|
@click.option("-k", "--kill", default=False, is_flag=True, help="Kill previous instance")
|
||||||
@click.option("-n", "--no-interaction", default=True, is_flag=True, help="Don't load password protected keys")
|
@click.option("-n", "--no-interaction", default=True, is_flag=True, help="Don't load password protected keys")
|
||||||
def certidude_spawn(kill, no_interaction):
|
def certidude_spawn_signer(kill, no_interaction):
|
||||||
"""
|
"""
|
||||||
Spawn processes for signers
|
Spawn privilege isolated signer process
|
||||||
"""
|
"""
|
||||||
from certidude import config
|
from certidude import config
|
||||||
|
|
||||||
@ -80,7 +232,6 @@ def certidude_spawn(kill, no_interaction):
|
|||||||
|
|
||||||
# Process directories
|
# Process directories
|
||||||
run_dir = "/run/certidude"
|
run_dir = "/run/certidude"
|
||||||
chroot_dir = os.path.join(run_dir, "jail")
|
|
||||||
|
|
||||||
# Prepare signer PID-s directory
|
# Prepare signer PID-s directory
|
||||||
if not os.path.exists(run_dir):
|
if not os.path.exists(run_dir):
|
||||||
@ -92,15 +243,13 @@ def certidude_spawn(kill, no_interaction):
|
|||||||
"".encode("charmap")
|
"".encode("charmap")
|
||||||
|
|
||||||
# Prepare chroot directories
|
# Prepare chroot directories
|
||||||
|
chroot_dir = os.path.join(run_dir, "jail")
|
||||||
if not os.path.exists(os.path.join(chroot_dir, "dev")):
|
if not os.path.exists(os.path.join(chroot_dir, "dev")):
|
||||||
os.makedirs(os.path.join(chroot_dir, "dev"))
|
os.makedirs(os.path.join(chroot_dir, "dev"))
|
||||||
if not os.path.exists(os.path.join(chroot_dir, "dev", "urandom")):
|
if not os.path.exists(os.path.join(chroot_dir, "dev", "urandom")):
|
||||||
# TODO: use os.mknod instead
|
# TODO: use os.mknod instead
|
||||||
os.system("mknod -m 444 %s c 1 9" % os.path.join(chroot_dir, "dev", "urandom"))
|
os.system("mknod -m 444 %s c 1 9" % os.path.join(chroot_dir, "dev", "urandom"))
|
||||||
|
|
||||||
ca_loaded = False
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config.SIGNER_PID_PATH) as fh:
|
with open(config.SIGNER_PID_PATH) as fh:
|
||||||
pid = int(fh.readline())
|
pid = int(fh.readline())
|
||||||
@ -122,26 +271,27 @@ def certidude_spawn(kill, no_interaction):
|
|||||||
|
|
||||||
child_pid = os.fork()
|
child_pid = os.fork()
|
||||||
|
|
||||||
if child_pid == 0:
|
if child_pid:
|
||||||
with open(config.SIGNER_PID_PATH, "w") as fh:
|
|
||||||
fh.write("%d\n" % os.getpid())
|
|
||||||
|
|
||||||
# setproctitle("%s spawn %s" % (sys.argv[0], ca.common_name))
|
|
||||||
logging.basicConfig(
|
|
||||||
filename="/var/log/signer.log",
|
|
||||||
level=logging.INFO)
|
|
||||||
server = SignServer(
|
|
||||||
config.SIGNER_SOCKET_PATH,
|
|
||||||
config.AUTHORITY_PRIVATE_KEY_PATH,
|
|
||||||
config.AUTHORITY_CERTIFICATE_PATH,
|
|
||||||
config.CERTIFICATE_LIFETIME,
|
|
||||||
config.CERTIFICATE_BASIC_CONSTRAINTS,
|
|
||||||
config.CERTIFICATE_KEY_USAGE_FLAGS,
|
|
||||||
config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
|
|
||||||
config.REVOCATION_LIST_LIFETIME)
|
|
||||||
asyncore.loop()
|
|
||||||
else:
|
|
||||||
click.echo("Spawned certidude signer process with PID %d at %s" % (child_pid, config.SIGNER_SOCKET_PATH))
|
click.echo("Spawned certidude signer process with PID %d at %s" % (child_pid, config.SIGNER_SOCKET_PATH))
|
||||||
|
return
|
||||||
|
|
||||||
|
setproctitle("certidude spawn signer" % section)
|
||||||
|
with open(config.SIGNER_PID_PATH, "w") as fh:
|
||||||
|
fh.write("%d\n" % os.getpid())
|
||||||
|
logging.basicConfig(
|
||||||
|
filename="/var/log/signer.log",
|
||||||
|
level=logging.INFO)
|
||||||
|
server = SignServer(
|
||||||
|
config.SIGNER_SOCKET_PATH,
|
||||||
|
config.AUTHORITY_PRIVATE_KEY_PATH,
|
||||||
|
config.AUTHORITY_CERTIFICATE_PATH,
|
||||||
|
config.CERTIFICATE_LIFETIME,
|
||||||
|
config.CERTIFICATE_BASIC_CONSTRAINTS,
|
||||||
|
config.CERTIFICATE_KEY_USAGE_FLAGS,
|
||||||
|
config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
|
||||||
|
config.REVOCATION_LIST_LIFETIME)
|
||||||
|
asyncore.loop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("client", help="Setup X.509 certificates for application")
|
@click.command("client", help="Setup X.509 certificates for application")
|
||||||
@ -894,6 +1044,9 @@ def certidude_setup_openvpn(): pass
|
|||||||
@click.group("setup", help="Getting started section")
|
@click.group("setup", help="Getting started section")
|
||||||
def certidude_setup(): pass
|
def certidude_setup(): pass
|
||||||
|
|
||||||
|
@click.group("spawn", help="Spawn helper processes")
|
||||||
|
def certidude_spawn(): pass
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def entry_point(): pass
|
def entry_point(): pass
|
||||||
|
|
||||||
@ -907,6 +1060,8 @@ certidude_setup.add_command(certidude_setup_openvpn)
|
|||||||
certidude_setup.add_command(certidude_setup_strongswan)
|
certidude_setup.add_command(certidude_setup_strongswan)
|
||||||
certidude_setup.add_command(certidude_setup_client)
|
certidude_setup.add_command(certidude_setup_client)
|
||||||
certidude_setup.add_command(certidude_setup_production)
|
certidude_setup.add_command(certidude_setup_production)
|
||||||
|
certidude_spawn.add_command(certidude_spawn_request)
|
||||||
|
certidude_spawn.add_command(certidude_spawn_signer)
|
||||||
entry_point.add_command(certidude_setup)
|
entry_point.add_command(certidude_setup)
|
||||||
entry_point.add_command(certidude_serve)
|
entry_point.add_command(certidude_serve)
|
||||||
entry_point.add_command(certidude_spawn)
|
entry_point.add_command(certidude_spawn)
|
||||||
|
12
certidude/errors.py
Normal file
12
certidude/errors.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
class RequestExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FatalError(Exception):
|
||||||
|
"""
|
||||||
|
Exception to be raised when user intervention is required
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DuplicateCommonNameError(FatalError):
|
||||||
|
pass
|
@ -1,11 +1,13 @@
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from certidude import errors
|
||||||
from certidude.wrappers import Certificate, Request
|
from certidude.wrappers import Certificate, Request
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None):
|
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_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):
|
||||||
"""
|
"""
|
||||||
Exchange CSR for certificate using Certidude HTTP API server
|
Exchange CSR for certificate using Certidude HTTP API server
|
||||||
"""
|
"""
|
||||||
@ -18,12 +20,10 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
|||||||
request_params.add("wait=forever")
|
request_params.add("wait=forever")
|
||||||
|
|
||||||
# Expand ca.example.com to http://ca.example.com/api/
|
# Expand ca.example.com to http://ca.example.com/api/
|
||||||
if not "/" in url:
|
if not url.endswith("/"):
|
||||||
url += "/api/"
|
url += "/api/"
|
||||||
if "//" not in url:
|
if "//" not in url:
|
||||||
url = "http://" + url
|
url = "http://" + url
|
||||||
if not url.endswith("/"):
|
|
||||||
url = url + "/"
|
|
||||||
|
|
||||||
authority_url = url + "certificate"
|
authority_url = url + "certificate"
|
||||||
request_url = url + "request"
|
request_url = url + "request"
|
||||||
@ -31,131 +31,116 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
|||||||
if request_params:
|
if request_params:
|
||||||
request_url = request_url + "?" + "&".join(request_params)
|
request_url = request_url + "?" + "&".join(request_params)
|
||||||
|
|
||||||
|
if os.path.exists(certificate_path):
|
||||||
|
click.echo("Found certificate: %s" % certificate_path)
|
||||||
|
# TODO: Check certificate validity, download CRL?
|
||||||
|
return
|
||||||
|
|
||||||
if os.path.exists(authority_path):
|
if os.path.exists(authority_path):
|
||||||
click.echo("Found CA certificate in: %s" % authority_path)
|
click.echo("Found CA certificate in: %s" % authority_path)
|
||||||
else:
|
else:
|
||||||
if authority_url:
|
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
|
||||||
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(authority_url) as fh:
|
r = requests.get(authority_url)
|
||||||
buf = fh.read()
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
|
||||||
try:
|
except crypto.Error:
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
raise ValueError("Failed to parse PEM: %s" % r.text)
|
||||||
except crypto.Error:
|
with open(authority_path + ".part", "w") as oh:
|
||||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
oh.write(r.text)
|
||||||
with open(authority_path + ".part", "wb") as oh:
|
click.echo("Writing CA certificate to: %s" % authority_path)
|
||||||
oh.write(buf)
|
os.rename(authority_path + ".part", authority_path)
|
||||||
click.echo("Writing CA certificate to: %s" % authority_path)
|
|
||||||
os.rename(authority_path + ".part", authority_path)
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
click.echo("Failed to fetch CA certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError("CA certificate not found and no URL specified")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
certificate = Certificate(open(certificate_path))
|
request = Request(open(request_path))
|
||||||
click.echo("Found certificate: %s" % certificate_path)
|
click.echo("Found signing request: %s" % request_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
try:
|
|
||||||
request = Request(open(request_path))
|
|
||||||
click.echo("Found signing request: %s" % request_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
|
|
||||||
# Construct private key
|
# Construct private key
|
||||||
click.echo("Generating 4096-bit RSA key...")
|
click.echo("Generating 4096-bit RSA key...")
|
||||||
key = crypto.PKey()
|
key = crypto.PKey()
|
||||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
key.generate_key(crypto.TYPE_RSA, 4096)
|
||||||
|
|
||||||
# Dump private key
|
# Dump private key
|
||||||
os.umask(0o077)
|
os.umask(0o077)
|
||||||
with open(key_path + ".part", "wb") as fh:
|
with open(key_path + ".part", "wb") as fh:
|
||||||
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
||||||
|
|
||||||
# Construct CSR
|
# Construct CSR
|
||||||
csr = crypto.X509Req()
|
csr = crypto.X509Req()
|
||||||
csr.set_version(2) # Corresponds to X.509v3
|
csr.set_version(2) # Corresponds to X.509v3
|
||||||
csr.set_pubkey(key)
|
csr.set_pubkey(key)
|
||||||
request = Request(csr)
|
request = Request(csr)
|
||||||
|
|
||||||
# Set subject attributes
|
# Set subject attributes
|
||||||
request.common_name = common_name
|
request.common_name = common_name
|
||||||
if given_name:
|
if given_name:
|
||||||
request.given_name = given_name
|
request.given_name = given_name
|
||||||
if surname:
|
if surname:
|
||||||
request.surname = surname
|
request.surname = surname
|
||||||
if org_unit:
|
if org_unit:
|
||||||
request.organizational_unit = org_unit
|
request.organizational_unit = org_unit
|
||||||
|
|
||||||
# Collect subject alternative names
|
# Collect subject alternative names
|
||||||
subject_alt_name = set()
|
subject_alt_name = set()
|
||||||
if email_address:
|
if email_address:
|
||||||
subject_alt_name.add("email:" + email_address)
|
subject_alt_name.add("email:" + email_address)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
subject_alt_name.add("IP:" + ip_address)
|
subject_alt_name.add("IP:" + ip_address)
|
||||||
if dns:
|
if dns:
|
||||||
subject_alt_name.add("DNS:" + dns)
|
subject_alt_name.add("DNS:" + dns)
|
||||||
|
|
||||||
# Set extensions
|
# Set extensions
|
||||||
extensions = []
|
extensions = []
|
||||||
if key_usage:
|
if key_usage:
|
||||||
extensions.append(("keyUsage", key_usage, True))
|
extensions.append(("keyUsage", key_usage, True))
|
||||||
if extended_key_usage:
|
if extended_key_usage:
|
||||||
extensions.append(("extendedKeyUsage", extended_key_usage, True))
|
extensions.append(("extendedKeyUsage", extended_key_usage, True))
|
||||||
if subject_alt_name:
|
if subject_alt_name:
|
||||||
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True))
|
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True))
|
||||||
request.set_extensions(extensions)
|
request.set_extensions(extensions)
|
||||||
|
|
||||||
# Dump CSR
|
# Dump CSR
|
||||||
os.umask(0o022)
|
os.umask(0o022)
|
||||||
with open(request_path + ".part", "w") as fh:
|
with open(request_path + ".part", "w") as fh:
|
||||||
fh.write(request.dump())
|
fh.write(request.dump())
|
||||||
|
|
||||||
click.echo("Writing private key to: %s" % key_path)
|
click.echo("Writing private key to: %s" % key_path)
|
||||||
os.rename(key_path + ".part", key_path)
|
os.rename(key_path + ".part", key_path)
|
||||||
click.echo("Writing certificate signing request to: %s" % request_path)
|
click.echo("Writing certificate signing request to: %s" % request_path)
|
||||||
os.rename(request_path + ".part", request_path)
|
os.rename(request_path + ".part", request_path)
|
||||||
|
|
||||||
|
|
||||||
with open(request_path, "rb") as fh:
|
click.echo("Submitting to %s, waiting for response..." % request_url)
|
||||||
buf = fh.read()
|
submission = requests.post(request_url,
|
||||||
submission = urllib.request.Request(request_url, buf)
|
data=open(request_path),
|
||||||
submission.add_header("User-Agent", "Certidude")
|
headers={"User-Agent": "Certidude", "Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert"})
|
||||||
submission.add_header("Content-Type", "application/pkcs10")
|
|
||||||
submission.add_header("Accept", "application/x-x509-user-cert")
|
|
||||||
|
|
||||||
click.echo("Submitting to %s, waiting for response..." % request_url)
|
if submission.status_code == requests.codes.ok:
|
||||||
try:
|
pass
|
||||||
response = urllib.request.urlopen(submission)
|
if submission.status_code == requests.codes.accepted:
|
||||||
buf = response.read()
|
# Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now
|
||||||
if response.code == 202:
|
return
|
||||||
click.echo("No waiting was requested and server responded with 202 Accepted, run this command again once the certificate is signed")
|
if submission.status_code == requests.codes.conflict:
|
||||||
return 1
|
raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite")
|
||||||
assert buf, "Server responded with no body, status code %d" % response.code
|
else:
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
submission.raise_for_status()
|
||||||
except crypto.Error:
|
|
||||||
if buf == b'-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n':
|
|
||||||
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
|
|
||||||
else:
|
|
||||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
if e.code == 409:
|
|
||||||
click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True)
|
|
||||||
return 2
|
|
||||||
else:
|
|
||||||
click.echo("Failed to fetch certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
|
||||||
return 3
|
|
||||||
else:
|
|
||||||
if response.code == 202:
|
|
||||||
click.echo("Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now", err=True)
|
|
||||||
return 254
|
|
||||||
|
|
||||||
os.umask(0o022)
|
if submission.text == '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n':
|
||||||
with open(certificate_path + ".part", "wb") as gh:
|
# Should the client retry or disable request submission?
|
||||||
gh.write(buf)
|
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
|
||||||
|
|
||||||
click.echo("Writing certificate to: %s" % certificate_path)
|
try:
|
||||||
os.rename(certificate_path + ".part", certificate_path)
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
|
||||||
|
except crypto.Error:
|
||||||
|
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||||
|
|
||||||
|
os.umask(0o022)
|
||||||
|
with open(certificate_path + ".part", "w") as fh:
|
||||||
|
fh.write(submission.text)
|
||||||
|
|
||||||
|
click.echo("Writing certificate to: %s" % certificate_path)
|
||||||
|
os.rename(certificate_path + ".part", certificate_path)
|
||||||
|
|
||||||
# TODO: Validate fetched certificate against CA
|
# TODO: Validate fetched certificate against CA
|
||||||
# TODO: Check that recevied certificate CN and pubkey match
|
# TODO: Check that recevied certificate CN and pubkey match
|
||||||
|
@ -4,6 +4,7 @@ cryptography==1.0
|
|||||||
falcon==0.3.0
|
falcon==0.3.0
|
||||||
humanize==0.5.1
|
humanize==0.5.1
|
||||||
idna==2.0
|
idna==2.0
|
||||||
|
ipsecparse==0.1.0
|
||||||
Jinja2==2.8
|
Jinja2==2.8
|
||||||
ldap3==0.9.8.8
|
ldap3==0.9.8.8
|
||||||
MarkupSafe==0.23
|
MarkupSafe==0.23
|
||||||
@ -14,5 +15,6 @@ pycrypto==2.6.1
|
|||||||
pykerberos==1.1.8
|
pykerberos==1.1.8
|
||||||
pyOpenSSL==0.15.1
|
pyOpenSSL==0.15.1
|
||||||
python-mimeparse==0.1.4
|
python-mimeparse==0.1.4
|
||||||
|
requests==2.2.1
|
||||||
setproctitle==1.1.9
|
setproctitle==1.1.9
|
||||||
six==1.9.0
|
six==1.9.0
|
||||||
|
Loading…
Reference in New Issue
Block a user