Skip to content
Snippets Groups Projects
Commit 28cbcd7b authored by Pavel Kácha's avatar Pavel Kácha
Browse files

Support for 'openssl ca' backend

parent 2fb036f5
No related branches found
No related tags found
No related merge requests found
...@@ -6,13 +6,22 @@ ...@@ -6,13 +6,22 @@
import sys import sys
import os import os
import time
import fcntl
import errno
import string import string
import random import random
import struct import struct
import operator
import argparse import argparse
import subprocess
import json import json
import logging import logging
import os.path as pth
import subprocess
import shlex
import tempfile
import M2Crypto
import ConfigParser
# *ph* server vulnerable to logjam, local openssl too new, use hammer to disable Diffie-Helmann # *ph* server vulnerable to logjam, local openssl too new, use hammer to disable Diffie-Helmann
import ssl import ssl
ssl._DEFAULT_CIPHERS += ":!DH" ssl._DEFAULT_CIPHERS += ":!DH"
...@@ -20,105 +29,55 @@ ssl._DEFAULT_CIPHERS += ":!DH" ...@@ -20,105 +29,55 @@ ssl._DEFAULT_CIPHERS += ":!DH"
import ejbcaws import ejbcaws
# usual path to warden server # usual path to warden server
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "warden-server")) sys.path.append(pth.join(pth.dirname(__file__), "..", "warden-server"))
import warden_server import warden_server
from warden_server import Request, ObjectBase, FileLogger, SysLogger, Server, expose, read_cfg from warden_server import Request, ObjectBase, FileLogger, SysLogger, Server, expose, read_cfg
class ClientDisabledError(Exception): class ClientDisabledError(Exception): pass
pass class ClientNotIssuableError(Exception): pass
class AuthenticationError(Exception): pass
class PopenError(Exception): pass
class EjbcaClient(object):
def __init__(self, registry, ejbca_data=None): class Client(object):
self.registry = registry
self.ejbca_data = ejbca_data or {}
@property
def admins(self):
return [u if not u.startswith("RFC822NAME") else u[11:] for u in self.ejbca_data["subjectAltName"].split(",")]
@admins.setter
def admins(self, emails):
self.ejbca_data["subjectAltName"] = ",".join(("RFC822NAME=%s" % e for e in emails))
@property
def name(self):
username = self.ejbca_data["username"]
if not username.endswith(self.registry.username_suffix):
raise ValueError(("Ejbca user username does not conform to config", self.ejbca_data))
return username[:-len(self.registry.username_suffix)]
@name.setter
def name(self, new):
self.ejbca_data["username"] = new + self.registry.username_suffix
self.ejbca_data["subjectDN"] = self.registry.subject_dn_template % new
@property
def enabled(self):
return self.ejbca_data["status"] != ejbcaws.STATUS_HISTORICAL
@enabled.setter
def enabled(self, new):
if self.enabled:
if not new:
self.ejbca_data["status"] = ejbcaws.STATUS_HISTORICAL
else:
if new:
self.ejbca_data["status"] = ejbcaws.STATUS_GENERATED
@property
def status(self):
s = self.ejbca_data["status"]
if s == ejbcaws.STATUS_NEW:
return "Issuable"
elif s == ejbcaws.STATUS_GENERATED:
return "Passive"
elif s == ejbcaws.STATUS_INITIALIZED:
return "New"
elif s == ejbcaws.STATUS_HISTORICAL:
return "Disabled"
else:
return "EJBCA status %d" % s
def get_certs(self): def __init__(self, name, admins=None, status=None, pwd=None, opaque=None):
return self.registry.ejbca.find_certs(self.ejbca_data["username"], validOnly=False) self.name = name
self.admins = admins or []
self.status = status or "New"
self.pwd = pwd
self.opaque = opaque or {}
def allow_new_cert(self, pwd=None): def update(self, admins=None, status=None, pwd=None):
if not self.enabled: if admins is not None:
self.admins = admins
if status:
if self.status == "Disabled" and status not in ("Passive", "Disabled"):
raise ClientDisabledError("This client is disabled") raise ClientDisabledError("This client is disabled")
self.ejbca_data["status"] = ejbcaws.STATUS_NEW self.status = status
if pwd is not None: self.pwd = pwd if status=="Issuable" and pwd else None
self.ejbca_data["password"] = pwd
self.ejbca_data["clearPwd"] = True
def new_cert(self, csr, pwd):
cert = self.registry.ejbca.pkcs10_request(
self.ejbca_data["username"],
pwd, csr, 0, ejbcaws.RESPONSETYPE_CERTIFICATE)
return cert
def __str__(self): def __str__(self):
return ( return (
"Client: %s (%s)\n" "Client: %s\n"
"Admins: %s\n" "Admins: %s\n"
"Status: %s\n" "Status: %s\n"
) % ( ) % (self.name, ", ".join(self.admins), self.status)
self.name,
self.ejbca_data["subjectDN"],
", ".join(self.admins),
self.status
)
def verbose_str(self): def str(self, verbose=False):
return "%s\n" % self.ejbca_data return str(self) + (str(self.opaque) if self.opaque and verbose else "")
def save(self):
self.registry.ejbca.edit_user(self.ejbca_data)
class EjbcaRegistry(OpenSSLRegistry):
class EjbcaRegistry(object): status_ejbca_to_str = {
ejbcaws.STATUS_NEW: "Issuable",
ejbcaws.STATUS_GENERATED: "Passive",
ejbcaws.STATUS_INITIALIZED: "New",
ejbcaws.STATUS_HISTORICAL: "Disabled"
}
status_str_to_ejbca = dict((v, k) for k, v in status_ejbca_to_str.items())
def __init__(self, log, url, cert=None, key=None, def __init__(self, log, url, cert=None, key=None,
ca_name="", certificate_profile_name="", end_entity_profile_name="", ca_name="", certificate_profile_name="", end_entity_profile_name="",
...@@ -131,8 +90,15 @@ class EjbcaRegistry(object): ...@@ -131,8 +90,15 @@ class EjbcaRegistry(object):
self.subject_dn_template = subject_dn_template self.subject_dn_template = subject_dn_template
self.username_suffix = username_suffix self.username_suffix = username_suffix
def client_data(self, ejbca_data):
ejbca_username = ejbca_data["username"]
username = ejbca_username[:-len(self.username_suffix)] if ejbca_username.endswith(self.username_suffix) else ejbca_username
admins = [u if not u.startswith("RFC822NAME") else u[11:] for u in ejbca_data["subjectAltName"].split(",")]
status = self.status_ejbca_to_str.get(ejbca_data["status"], "Other")
return username, admins, status, None, ejbca_data
def get_clients(self): def get_clients(self):
return (EjbcaClient(registry=self, ejbca_data=data) for data in self.ejbca.get_users()) return [Client(*self.client_data(u)) for u in self.ejbca.get_users()]
def get_client(self, name): def get_client(self, name):
users = self.ejbca.find_user(ejbcaws.MATCH_WITH_USERNAME, ejbcaws.MATCH_TYPE_EQUALS, name + self.username_suffix) users = self.ejbca.find_user(ejbcaws.MATCH_WITH_USERNAME, ejbcaws.MATCH_TYPE_EQUALS, name + self.username_suffix)
...@@ -140,35 +106,151 @@ class EjbcaRegistry(object): ...@@ -140,35 +106,151 @@ class EjbcaRegistry(object):
raise LookupError("%d users %s found (more than one?!)" % (len(users), name)) raise LookupError("%d users %s found (more than one?!)" % (len(users), name))
if not users: if not users:
return None return None
return EjbcaClient(registry=self, ejbca_data=users[0]) return Client(*self.client_data(users[0]))
def new_client(self, name, admins): def save_client(self, client):
user = self.get_client(name) edata = client.opaque or dict(
if user:
raise LookupError("Client %s already exists" % name)
new_ejbca_data = dict(
caName=self.ca_name, caName=self.ca_name,
certificateProfileName=self.certificate_profile_name, certificateProfileName=self.certificate_profile_name,
endEntityProfileName=self.end_entity_profile_name, endEntityProfileName=self.end_entity_profile_name,
keyRecoverable=False, keyRecoverable=False,
sendNotification=False, sendNotification=False,
status=ejbcaws.STATUS_INITIALIZED,
subjectAltName="",
subjectDN="",
tokenType=ejbcaws.TOKEN_TYPE_USERGENERATED, tokenType=ejbcaws.TOKEN_TYPE_USERGENERATED,
username="",
password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16))), password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16))),
clearPwd = True clearPwd = True,
username = client.name + self.username_suffix,
subjectDN = self.subject_dn_template % client.name
) )
client = EjbcaClient(registry=self, ejbca_data=new_ejbca_data) edata["subjectAltName"] = ",".join(("RFC822NAME=%s" % a for a in client.admins))
client.name = name edata["status"] = self.status_str_to_ejbca.get(client.status, edata["status"])
client.admins = admins if client.pwd:
return client edata["password"] = client.pwd
edata["clearPwd"] = True
self.ejbca.edit_user(edata)
def get_certs(self, client):
return self.ejbca.find_certs(client.opaque["username"], validOnly=False)
def new_cert(self, client, csr, pwd):
cert = self.ejbca.pkcs10_request(
client.opaque["username"],
pwd, csr, 0, ejbcaws.RESPONSETYPE_CERTIFICATE)
return cert
def verbose_str(self): def __str__(self):
return self.ejbca.get_version() return self.ejbca.get_version()
class OpenSSLRegistry(object):
def __init__(self, log, base_dir,
subject_dn_template, openssl_sign, lock_timeout):
self.base_dir = base_dir
self.cnf_file = pth.join(base_dir, "openssl.cnf")
self.client_dir = pth.join(base_dir, "clients")
self.serial_file = pth.join(base_dir, "serial")
self.newcerts_dir = pth.join(base_dir, "newcerts")
self.csr_dir = pth.join(base_dir, "csr")
self.lock_file = pth.join(base_dir, "lock")
self.lock_timeout = lock_timeout
self.log = log
self.subject_dn_template = subject_dn_template
self.openssl_sign = openssl_sign
def get_clients(self):
return [self.get_client(c) for c in os.listdir(self.client_dir) if pth.isdir(pth.join(self.client_dir, c))]
def get_client(self, name):
config = ConfigParser.RawConfigParser()
if not config.read(pth.join(self.client_dir, name, "state")):
return None
datum = dict(config.items("Client"))
return Client(name, admins=datum["admins"].split(","), status=datum["status"], pwd=datum.get("password"))
def new_client(self, name, admins=None):
user = self.get_client(name)
if user:
raise LookupError("Client %s already exists" % name)
return Client(name, admins)
def save_client(self, client):
config = ConfigParser.RawConfigParser()
config.add_section("Client")
config.set("Client", "admins", ",".join(client.admins))
config.set("Client", "status", client.status)
if client.pwd:
config.set("Client", "password", client.pwd)
client_path = pth.join(self.client_dir, client.name)
try:
os.makedirs(client_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
with tempfile.NamedTemporaryFile(dir=client_path, delete=False) as cf:
config.write(cf)
os.rename(cf.name, pth.join(client_path, "state")) # atomic + rewrite, so no need for locking
def get_certs(self, client):
files = [fname for fname in os.listdir(pth.join(self.client_dir, client.name)) if not fname.startswith(".") and fname.endswith(".pem")]
certs = [M2Crypto.X509.load_cert(pth.join(self.client_dir, client.name, fname)) for fname in files]
return certs
def __enter__(self):
self._lockfd = os.open(self.lock_file, os.O_CREAT)
start = time.time()
while True:
try:
fcntl.flock(self._lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return
except (OSError, IOError) as e:
if e.errno != errno.EAGAIN or time.time() > start + self.lock_timeout:
raise
time.sleep(0.5)
def __exit__(self, type_, value, traceback):
fcntl.flock(self._lockfd, fcntl.LOCK_UN)
os.close(self._lockfd)
try:
os.unlink(self.lock_file)
except:
pass
def run_openssl(self, command, **kwargs):
cmdline = shlex.split(command % kwargs)
process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
res = process.communicate()
if process.returncode:
raise PopenError("Popen returned nonzero code", process.returncode, ' '.join(cmdline), res[0], res[1])
return res
def new_cert(self, client, csr, pwd):
if client.status != "Issuable" or not client.pwd:
raise ClientNotIssuableError("Client not allowed to issue request or password not set")
if client.pwd != pwd:
raise AuthenticationError("Wrong credentials")
dn = self.subject_dn_template.replace("/", "//").replace(",", "/") % client.name
if not dn.startswith("/"):
dn = "/" + dn
with tempfile.NamedTemporaryFile(dir=self.csr_dir, delete=False) as csr_file:
csr_file.write(csr)
with self: # lock dance
with open(self.serial_file) as f:
serial = f.read().strip()
output = self.run_openssl(self.openssl_sign, cnf = self.cnf_file, csr = csr_file.name, dn = dn)
self.log.debug(output)
os.rename(csr_file.name, pth.join(self.csr_dir, serial + ".csr.pem"))
client_pem_name = pth.join(self.client_dir, client.name, serial + ".cert.pem")
os.symlink(pth.join(self.newcerts_dir, serial + ".pem"), client_pem_name)
with open(client_pem_name) as pem:
cert = M2Crypto.X509.load_cert_string(pem.read(), M2Crypto.X509.FORMAT_PEM)
client.update(status="Passive", pwd=None)
self.save_client(client)
return cert
def __str__(self):
return "%s<%s>" % (type(self).__name__, self.base_dir)
def format_cert(cert): def format_cert(cert):
return ( return (
"Subject: %s\n" "Subject: %s\n"
...@@ -186,6 +268,7 @@ def format_cert(cert): ...@@ -186,6 +268,7 @@ def format_cert(cert):
cert.get_issuer().as_text() cert.get_issuer().as_text()
) )
# Server side # Server side
class OptionalAuthenticator(ObjectBase): class OptionalAuthenticator(ObjectBase):
...@@ -242,21 +325,21 @@ class CertHandler(ObjectBase): ...@@ -242,21 +325,21 @@ class CertHandler(ObjectBase):
client = self.registry.get_client(name[0]) client = self.registry.get_client(name[0])
if not client: if not client:
raise self.req.error(message="Unknown client", error=403, name=name, password=password) raise self.req.error(message="Unknown client", error=403, name=name, password=password)
self.log.info("Client %s" % client.name) self.log.info("Client %s" % client)
if self.req.client == "cert": if self.req.client == "cert":
# Correctly authenticated by cert, most probably not preactivated with password, # Correctly authenticated by cert, most probably not preactivated with password,
# so generate oneshot password and allow now # so generate oneshot password and allow now
password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16))) password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16)))
self.log.debug("Authorized by X509, enabling cert generation with password %s" % password) self.log.debug("Authorized by X509, enabling cert generation with password %s" % password)
try: try:
client.allow_new_cert(pwd=password) client.update(status="Issuable", pwd=password)
self.registry.save_client(client)
except ClientDisabledError as e: except ClientDisabledError as e:
raise self.req.error(message="Error enabling cert generation", error=403, exc=sys.exc_info()) raise self.req.error(message="Error enabling cert generation", error=403, exc=sys.exc_info())
client.save()
if not password: if not password:
raise self.req.error(message="Missing password and certificate validation failed", error=403, name=name, password=password) raise self.req.error(message="Missing password and certificate validation failed", error=403, name=name, password=password)
try: try:
newcert = client.new_cert(csr_data, password) newcert = self.registry.new_cert(client, csr_data, password)
except Exception as e: except Exception as e:
raise self.req.error(message="Processing error", error=403, exc=sys.exc_info()) raise self.req.error(message="Processing error", error=403, exc=sys.exc_info())
self.log.info("Generated.") self.log.info("Generated.")
...@@ -272,7 +355,7 @@ section_order = ("log", "auth", "registry", "handler", "server") ...@@ -272,7 +355,7 @@ section_order = ("log", "auth", "registry", "handler", "server")
section_def = { section_def = {
"log": [FileLogger, SysLogger], "log": [FileLogger, SysLogger],
"auth": [OptionalAuthenticator], "auth": [OptionalAuthenticator],
"registry": [EjbcaRegistry], "registry": [OpenSSLRegistry, EjbcaRegistry],
"handler": [CertHandler], "handler": [CertHandler],
"server": [Server] "server": [Server]
} }
...@@ -286,11 +369,18 @@ param_def = { ...@@ -286,11 +369,18 @@ param_def = {
"req": {"type": "obj", "default": "req"}, "req": {"type": "obj", "default": "req"},
"log": {"type": "obj", "default": "log"} "log": {"type": "obj", "default": "log"}
}, },
OpenSSLRegistry: {
"log": {"type": "obj", "default": "log"},
"base_dir": {"type": "str", "default": pth.join(pth.dirname(__file__), "ca")},
"subject_dn_template": {"type": "str", "default": "DC=cz,DC=example-ca,DC=warden,CN=%s"},
"openssl_sign": {"type": "str", "default": "openssl ca -config %(cnf)s -batch -extensions server_cert -days 375 -notext -md sha256 -in %(csr)s -subj '%(dn)s'"},
"lock_timeout": {"type": "natural", "default": "3"}
},
EjbcaRegistry: { EjbcaRegistry: {
"log": {"type": "obj", "default": "log"}, "log": {"type": "obj", "default": "log"},
"url": {"type": "str", "default": "https://ejbca.example.org/ejbca/ejbcaws/ejbcaws?wsdl"}, "url": {"type": "str", "default": "https://ejbca.example.org/ejbca/ejbcaws/ejbcaws?wsdl"},
"cert": {"type": "filepath", "default": os.path.join(os.path.dirname(__file__), "warden_ra.cert.pem")}, "cert": {"type": "filepath", "default": pth.join(pth.dirname(__file__), "warden_ra.cert.pem")},
"key": {"type": "filepath", "default": os.path.join(os.path.dirname(__file__), "warden_ra.key.pem")}, "key": {"type": "filepath", "default": pth.join(pth.dirname(__file__), "warden_ra.key.pem")},
"ca_name": {"type": "str", "default": "Example CA"}, "ca_name": {"type": "str", "default": "Example CA"},
"certificate_profile_name": {"type": "str", "default": "Example"}, "certificate_profile_name": {"type": "str", "default": "Example"},
"end_entity_profile_name": {"type": "str", "default": "Example EE"}, "end_entity_profile_name": {"type": "str", "default": "Example EE"},
...@@ -304,7 +394,7 @@ param_def = { ...@@ -304,7 +394,7 @@ param_def = {
} }
} }
param_def[FileLogger]["filename"] = {"type": "filepath", "default": os.path.join(os.path.dirname(__file__), os.path.splitext(os.path.split(__file__)[1])[0] + ".log")} param_def[FileLogger]["filename"] = {"type": "filepath", "default": pth.join(pth.dirname(__file__), pth.splitext(pth.split(__file__)[1])[0] + ".log")}
def build_server(conf): def build_server(conf):
...@@ -313,36 +403,33 @@ def build_server(conf): ...@@ -313,36 +403,33 @@ def build_server(conf):
# Command line # Command line
def list_clients(registry, name=None, verbose=False): def list_clients(registry, name=None, verbose=False, show_cert=True):
if name is not None: if name is not None:
client = registry.get_client(name) client = registry.get_client(name)
if client is None: if client is None:
print "No such client." print "No such client."
return return
else: else:
print(client) print(client.str(verbose))
if verbose: if show_cert:
print(client.verbose_str()) for cert in sorted(registry.get_certs(client), key=lambda c: c.get_not_after().get_datetime()):
for cert in sorted(client.get_certs(), key=lambda c: c.get_not_after().get_datetime()):
print(format_cert(cert)) print(format_cert(cert))
if verbose: if verbose:
print(cert.as_text()) print(cert.as_text())
else: else:
clients = registry.get_clients() clients = registry.get_clients()
for client in sorted (clients, key=lambda c: c.name): for client in sorted(clients, key=operator.attrgetter("name")):
print(client) print(client.str(verbose))
if verbose:
print(client.verbose_str())
def register_client(registry, name, admins=None, verbose=False): def register_client(registry, name, admins=None, verbose=False):
try: try:
client = registry.new_client(name, admins or []) client = registry.new_client(name, admins)
except LookupError as e: except LookupError as e:
print(e) print(e)
return return
client.save() registry.save_client(client)
list_clients(registry, name, verbose) list_clients(registry, name, verbose, show_cert=False)
def applicant(registry, name, password=None, verbose=False): def applicant(registry, name, password=None, verbose=False):
...@@ -353,12 +440,12 @@ def applicant(registry, name, password=None, verbose=False): ...@@ -353,12 +440,12 @@ def applicant(registry, name, password=None, verbose=False):
if password is None: if password is None:
password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16))) password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16)))
try: try:
client.allow_new_cert(pwd=password) client.update(status="Issuable", pwd=password)
except ClientDisabledError: except ClientDisabledError:
print "This client is disabled. Use 'enable' first." print "This client is disabled. Use 'enable' first."
return return
client.save() registry.save_client(client)
list_clients(registry, name, verbose) list_clients(registry, name, verbose, show_cert=False)
print("Application password is: %s\n" % password) print("Application password is: %s\n" % password)
...@@ -367,9 +454,9 @@ def enable(registry, name, verbose=False): ...@@ -367,9 +454,9 @@ def enable(registry, name, verbose=False):
if not client: if not client:
print "No such client." print "No such client."
return return
client.enabled = True client.update(status="Passive")
client.save() registry.save_client(client)
list_clients(registry, name, verbose) list_clients(registry, name, verbose, show_cert=False)
def disable(registry, name, verbose=False): def disable(registry, name, verbose=False):
...@@ -377,9 +464,9 @@ def disable(registry, name, verbose=False): ...@@ -377,9 +464,9 @@ def disable(registry, name, verbose=False):
if not client: if not client:
print "No such client." print "No such client."
return return
client.enabled = False client.update(status="Disabled")
client.save() registry.save_client(client)
list_clients(registry, name, verbose) list_clients(registry, name, verbose, show_cert=False)
def request(registry, key, csr, verbose=False): def request(registry, key, csr, verbose=False):
...@@ -409,7 +496,7 @@ def gen_cert(registry, name, csr, cert, password, verbose=False): ...@@ -409,7 +496,7 @@ def gen_cert(registry, name, csr, cert, password, verbose=False):
with open(csr, "r") as f: with open(csr, "r") as f:
csr_data = f.read() csr_data = f.read()
client = registry.get_client(name) client = registry.get_client(name)
newcert = client.new_cert(csr_data, password) newcert = registry.new_cert(client, csr_data, password)
print(format_cert(newcert)) print(format_cert(newcert))
if verbose: if verbose:
print(newcert.as_text()) print(newcert.as_text())
...@@ -510,7 +597,7 @@ def get_args(): ...@@ -510,7 +597,7 @@ def get_args():
if __name__ == "__main__": if __name__ == "__main__":
args = get_args() args = get_args()
config = os.path.join(os.path.dirname(__file__), args.config or "warden_ra.cfg") config = pth.join(pth.dirname(__file__), args.config or "warden_ra.cfg")
server = build_server(read_cfg(config)) server = build_server(read_cfg(config))
registry = server.handler.registry registry = server.handler.registry
if args.verbose: if args.verbose:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment