From 92c7487cc1ad0d5b9de960cf9974d15e5c88b77a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20K=C3=A1cha?= <ph@cesnet.cz>
Date: Mon, 13 Jun 2016 14:32:24 +0200
Subject: [PATCH] Warden certificate registration authority initial commit

---
 warden3/contrib/warden_ra/ejbcaws.py    | 231 +++++++++++++++++
 warden3/contrib/warden_ra/warden_ra.cfg |  10 +
 warden3/contrib/warden_ra/warden_ra.py  | 326 ++++++++++++++++++++++++
 3 files changed, 567 insertions(+)
 create mode 100644 warden3/contrib/warden_ra/ejbcaws.py
 create mode 100644 warden3/contrib/warden_ra/warden_ra.cfg
 create mode 100755 warden3/contrib/warden_ra/warden_ra.py

diff --git a/warden3/contrib/warden_ra/ejbcaws.py b/warden3/contrib/warden_ra/ejbcaws.py
new file mode 100644
index 0000000..42c4828
--- /dev/null
+++ b/warden3/contrib/warden_ra/ejbcaws.py
@@ -0,0 +1,231 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, CESNET, z. s. p. o.
+# Use of this source is governed by an ISC license, see LICENSE file.
+
+import urllib2
+import httplib
+import socket
+import base64
+import suds.transport.http
+import suds.client
+import M2Crypto
+
+
+STATUS_FAILED = 11
+STATUS_GENERATED = 40
+STATUS_HISTORICAL = 60
+STATUS_INITIALIZED = 20
+STATUS_INPROCESS = 30
+STATUS_KEYRECOVERY = 70
+STATUS_NEW = 10
+STATUS_REVOKED = 50
+
+MATCH_TYPE_BEGINSWITH = 1
+MATCH_TYPE_CONTAINS = 2
+MATCH_TYPE_EQUALS = 0
+MATCH_WITH_CA = 5
+MATCH_WITH_CERTIFICATEPROFILE = 4
+MATCH_WITH_COMMONNAME = 101
+MATCH_WITH_COUNTRY = 112
+MATCH_WITH_DIRECTORYNAME = 204
+MATCH_WITH_DN = 7
+MATCH_WITH_DNSERIALNUMBER = 102
+MATCH_WITH_DNSNAME = 201
+MATCH_WITH_DOMAINCOMPONENT = 111
+MATCH_WITH_EDIPARTNAME = 205
+MATCH_WITH_EMAIL = 1
+MATCH_WITH_ENDENTITYPROFILE = 3
+MATCH_WITH_GIVENNAME = 103
+MATCH_WITH_GUID = 209
+MATCH_WITH_INITIALS = 104
+MATCH_WITH_IPADDRESS = 202
+MATCH_WITH_LOCALE = 109
+MATCH_WITH_ORGANIZATION = 108
+MATCH_WITH_ORGANIZATIONUNIT = 107
+MATCH_WITH_REGISTEREDID = 207
+MATCH_WITH_RFC822NAME = 200
+MATCH_WITH_STATE = 110
+MATCH_WITH_STATUS = 2
+MATCH_WITH_SURNAME = 105
+MATCH_WITH_TITLE = 106
+MATCH_WITH_TOKEN = 6
+MATCH_WITH_UID = 100
+MATCH_WITH_UPN = 208
+MATCH_WITH_URI = 206
+MATCH_WITH_USERNAME = 0
+MATCH_WITH_X400ADDRESS = 203
+
+TOKEN_TYPE_JKS = "JKS"
+TOKEN_TYPE_P12 = "P12"
+TOKEN_TYPE_PEM = "PEM"
+TOKEN_TYPE_USERGENERATED = "USERGENERATED"
+
+VIEW_RIGHTS = "/view_end_entity"
+EDIT_RIGHTS = "/edit_end_entity"
+CREATE_RIGHTS = "/create_end_entity"
+DELETE_RIGHTS = "/delete_end_entity"
+REVOKE_RIGHTS = "/revoke_end_entity"
+HISTORY_RIGHTS = "/view_end_entity_history"
+APPROVAL_RIGHTS = "/approve_end_entity"
+HARDTOKEN_RIGHTS = "/view_hardtoken"
+HARDTOKEN_PUKDATA_RIGHTS = "/view_hardtoken/puk_data"
+KEYRECOVERY_RIGHTS = "/keyrecovery"
+
+ENDENTITYPROFILEBASE = "/endentityprofilesrules"
+ENDENTITYPROFILEPREFIX = "/endentityprofilesrules/"
+USERDATASOURCEBASE = "/userdatasourcesrules"
+USERDATASOURCEPREFIX = "/userdatasourcesrules/"
+UDS_FETCH_RIGHTS = "/fetch_userdata"
+UDS_REMOVE_RIGHTS = "/remove_userdata"
+
+CABASE = "/ca"
+CAPREFIX = "/ca/"
+ROLE_PUBLICWEBUSER = "/public_web_user"
+ROLE_ADMINISTRATOR = "/administrator"
+ROLE_SUPERADMINISTRATOR = "/super_administrator"
+REGULAR_CAFUNCTIONALTY = "/ca_functionality"
+REGULAR_CABASICFUNCTIONS = "/ca_functionality/basic_functions"
+REGULAR_ACTIVATECA = "/ca_functionality/basic_functions/activate_ca"
+REGULAR_RENEWCA = "/ca_functionality/renew_ca"
+REGULAR_VIEWCERTIFICATE = "/ca_functionality/view_certificate"
+REGULAR_APPROVECAACTION = "/ca_functionality/approve_caaction"
+REGULAR_CREATECRL = "/ca_functionality/create_crl"
+REGULAR_EDITCERTIFICATEPROFILES = "/ca_functionality/edit_certificate_profiles"
+REGULAR_CREATECERTIFICATE = "/ca_functionality/create_certificate"
+REGULAR_STORECERTIFICATE = "/ca_functionality/store_certificate"
+REGULAR_RAFUNCTIONALITY = "/ra_functionality"
+REGULAR_EDITENDENTITYPROFILES = "/ra_functionality/edit_end_entity_profiles"
+REGULAR_EDITUSERDATASOURCES = "/ra_functionality/edit_user_data_sources"
+REGULAR_VIEWENDENTITY = "/ra_functionality/view_end_entity"
+REGULAR_CREATEENDENTITY = "/ra_functionality/create_end_entity"
+REGULAR_EDITENDENTITY = "/ra_functionality/edit_end_entity"
+REGULAR_DELETEENDENTITY = "/ra_functionality/delete_end_entity"
+REGULAR_REVOKEENDENTITY = "/ra_functionality/revoke_end_entity"
+REGULAR_VIEWENDENTITYHISTORY = "/ra_functionality/view_end_entity_history"
+REGULAR_APPROVEENDENTITY = "/ra_functionality/approve_end_entity"
+REGULAR_LOGFUNCTIONALITY = "/log_functionality"
+REGULAR_VIEWLOG = "/log_functionality/view_log"
+REGULAR_LOGCONFIGURATION = "/log_functionality/edit_log_configuration"
+REGULAR_LOG_CUSTOM_EVENTS = "/log_functionality/log_custom_events"
+REGULAR_SYSTEMFUNCTIONALITY = "/system_functionality"
+REGULAR_EDITADMINISTRATORPRIVILEDGES = "/system_functionality/edit_administrator_privileges"
+REGULAR_EDITSYSTEMCONFIGURATION = "/system_functionality/edit_systemconfiguration"
+REGULAR_VIEWHARDTOKENS = "/ra_functionality/view_hardtoken"
+REGULAR_VIEWPUKS = "/ra_functionality/view_hardtoken/puk_data"
+REGULAR_KEYRECOVERY = "/ra_functionality/keyrecovery"
+
+HARDTOKEN_HARDTOKENFUNCTIONALITY = "/hardtoken_functionality"
+HARDTOKEN_EDITHARDTOKENISSUERS = "/hardtoken_functionality/edit_hardtoken_issuers"
+HARDTOKEN_EDITHARDTOKENPROFILES = "/hardtoken_functionality/edit_hardtoken_profiles"
+HARDTOKEN_ISSUEHARDTOKENS = "/hardtoken_functionality/issue_hardtokens"
+HARDTOKEN_ISSUEHARDTOKENADMINISTRATORS = "/hardtoken_functionality/issue_hardtoken_administrators"
+
+RESPONSETYPE_CERTIFICATE = "CERTIFICATE"
+RESPONSETYPE_PKCS7 = "PKCS7"
+RESPONSETYPE_PKCS7WITHCHAIN = "PKCS7WITHCHAIN"
+
+NOT_REVOKED = -1
+REVOKATION_REASON_UNSPECIFIED = 0
+REVOKATION_REASON_KEYCOMPROMISE = 1
+REVOKATION_REASON_CACOMPROMISE = 2
+REVOKATION_REASON_AFFILIATIONCHANGED = 3
+REVOKATION_REASON_SUPERSEDED = 4
+REVOKATION_REASON_CESSATIONOFOPERATION = 5
+REVOKATION_REASON_CERTIFICATEHOLD = 6
+REVOKATION_REASON_REMOVEFROMCRL = 8
+REVOKATION_REASON_PRIVILEGESWITHDRAWN = 9
+REVOKATION_REASON_AACOMPROMISE = 10
+
+
+class HTTPSClientAuthHandler(urllib2.HTTPSHandler):
+
+    def __init__(self, key, cert):
+        urllib2.HTTPSHandler.__init__(self)
+        self.key = key
+        self.cert = cert
+
+    def https_open(self, req):
+        return self.do_open(self.get_connection, req, context=self._context)
+
+    def get_connection(self, host, timeout=5, context=None):
+        return httplib.HTTPSConnection(host, key_file=self.key, cert_file=self.cert, timeout=timeout, context=context)
+
+
+class HTTPSClientCertTransport(suds.transport.http.HttpTransport):
+
+    def __init__(self, key, cert, *args, **kwargs):
+        suds.transport.http.HttpTransport.__init__(self, *args, **kwargs)
+        self.key = key
+        self.cert = cert
+
+    def u2open(self, u2request):
+        tm = self.options.timeout
+        url = urllib2.build_opener(HTTPSClientAuthHandler(self.key, self.cert))
+        if self.u2ver() < 2.6:
+            socket.setdefaulttimeout(tm)
+            return url.open(u2request)
+        else:
+            return url.open(u2request, timeout=tm)
+
+
+class Ejbca(object):
+
+    def __init__(self, url, cert=None, key=None):
+        self.url = url
+        self.cert = cert
+        self.key = key
+        self.transport = HTTPSClientCertTransport(self.key, self.cert) if self.cert else None
+        self.wsclient = suds.client.Client(self.url, transport=self.transport)
+
+
+    def get_version(self):
+        return self.wsclient.service.getEjbcaVersion()
+
+
+    def get_users(self):
+        return self.find_user(MATCH_WITH_DN, MATCH_TYPE_CONTAINS, "=")
+
+
+    def find_user(self, matchwith, matchtype, matchvalue):
+        usermatch = self.wsclient.factory.create('userMatch')
+        usermatch.matchwith = matchwith
+        usermatch.matchtype = matchtype
+        usermatch.matchvalue = matchvalue
+        return self.wsclient.service.findUser(usermatch)
+
+
+    def edit_user(self, user):
+        return self.wsclient.service.editUser(user)
+
+
+    def _decode_ejbca_cert(self, double_mess):
+        single_mess = base64.b64decode(double_mess)
+        cert_data = base64.b64decode(single_mess)
+        cert = M2Crypto.X509.load_cert_string(cert_data, M2Crypto.X509.FORMAT_DER)
+        return cert
+
+
+    def pkcs10_request(self, username, password, pkcs10, hardTokenSN, responseType):
+        res = self.wsclient.service.pkcs10Request(
+            arg0=username,
+            arg1=password,
+            arg2=pkcs10,
+            arg3=hardTokenSN,
+            arg4=responseType)
+        return self._decode_ejbca_cert(res["data"])
+
+
+    def find_certs(self, loginName, validOnly=False):
+        reslist = self.wsclient.service.findCerts(
+            arg0=loginName,
+            arg1=validOnly)
+        certs = []
+        for res in reslist:
+            double_mess = res["certificateData"]
+            if double_mess is not None:
+                cert = self._decode_ejbca_cert(double_mess)
+                cert.ejbca_status = res["type"]
+                certs.append(cert)
+        return certs
diff --git a/warden3/contrib/warden_ra/warden_ra.cfg b/warden3/contrib/warden_ra/warden_ra.cfg
new file mode 100644
index 0000000..27308d0
--- /dev/null
+++ b/warden3/contrib/warden_ra/warden_ra.cfg
@@ -0,0 +1,10 @@
+{
+    "url": "https://ejbca.example.org/ejbca/ejbcaws/ejbcaws?wsdl",
+    "cert": "warden_ra.cert.pem",
+    "key": "warden_ra.key.pem",
+    "caName": "Example CA",
+    "certificateProfileName": "Example",
+    "endEntityProfileName": "Example EE",
+    "subjectDN_template": "DC=cz,DC=example-ca,DC=warden,CN=%s",
+    "username_suffix": "@warden"
+}
\ No newline at end of file
diff --git a/warden3/contrib/warden_ra/warden_ra.py b/warden3/contrib/warden_ra/warden_ra.py
new file mode 100755
index 0000000..68a98d0
--- /dev/null
+++ b/warden3/contrib/warden_ra/warden_ra.py
@@ -0,0 +1,326 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, CESNET, z. s. p. o.
+# Use of this source is governed by an ISC license, see LICENSE file.
+
+import sys
+import os
+import string
+import random
+import struct
+import argparse
+import subprocess
+import json
+# *ph* server vulnerable to logjam, local openssl too new, use hammer to disable Diffie-Helmann
+import ssl
+ssl._DEFAULT_CIPHERS += ":!DH"
+
+import ejbcaws
+
+
+class EjbcaClient(object):
+
+    def __init__(self, registry, ejbca_data=None):
+        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.subjectDN_template % new
+
+    @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"
+        else:
+            return "EJBCA status %d" % s
+
+    def get_certs(self):
+        return self.registry.ejbca.find_certs(self.ejbca_data["username"], validOnly=False)
+
+    def allow_new_cert(self, pwd=None):
+        self.ejbca_data["status"] = ejbcaws.STATUS_NEW
+        if pwd is not 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):
+        return (
+            "Client:   %s (%s)\n"
+            "Admins:   %s\n"
+            "Status:   %s\n"
+        ) % (
+            self.name,
+            self.ejbca_data["subjectDN"],
+            ", ".join(self.admins),
+            self.status
+        )
+
+    def verbose_str(self):
+        return "%s\n" % self.ejbca_data
+
+    def save(self):
+        self.registry.ejbca.edit_user(self.ejbca_data)
+
+
+class EjbcaRegistry(object):
+
+    def __init__(self, url, cert=None, key=None,
+                 caName="", certificateProfileName="", endEntityProfileName="",
+                 subjectDN_template="%s", username_suffix=""):
+        self.ejbca = ejbcaws.Ejbca(url, cert, key)
+        self.caName = caName
+        self.certificateProfileName = certificateProfileName
+        self.endEntityProfileName = endEntityProfileName
+        self.subjectDN_template = subjectDN_template
+        self.username_suffix = username_suffix
+
+    def get_clients(self):
+        return (EjbcaClient(registry=self, ejbca_data=data) for data in self.ejbca.get_users())
+
+    def get_client(self, name):
+        users = self.ejbca.find_user(ejbcaws.MATCH_WITH_USERNAME, ejbcaws.MATCH_TYPE_EQUALS, name + self.username_suffix)
+        if len(users) > 1:
+            raise LookupError("%d users %s found (more than one?!)" % (len(users), name))
+        if not users:
+            return None
+        return EjbcaClient(registry=self, ejbca_data=users[0])
+
+    def new_client(self, name, admins):
+        user = self.get_client(name)
+        if user:
+            raise LookupError("Client %s already exists" % name)
+        new_ejbca_data = dict(
+            caName=self.caName,
+            certificateProfileName=self.certificateProfileName,
+            endEntityProfileName=self.endEntityProfileName,
+            keyRecoverable=False,
+            sendNotification=False,
+            status=ejbcaws.STATUS_INITIALIZED,
+            subjectAltName="",
+            subjectDN="",
+            tokenType=ejbcaws.TOKEN_TYPE_USERGENERATED,
+            username="")
+        client = EjbcaClient(registry=self, ejbca_data=new_ejbca_data)
+        client.name = name
+        client.admins = admins
+        return client
+
+    def verbose_str(self):
+        return self.ejbca.get_version()
+
+
+def format_cert(cert):
+    return (
+        "Subject:     %s\n"
+        "Validity:    %s - %s\n"
+        "Serial:      %s\n"
+        "Fingerprint: md5:%s, sha1:%s\n"
+        "Issuer:      %s\n"
+    ) % (
+        cert.get_subject().as_text(),
+        cert.get_not_before().get_datetime().isoformat(),
+        cert.get_not_after().get_datetime().isoformat(),
+        ":".join(["%02x" % ord(c) for c in struct.pack('!Q', cert.get_serial_number())]),
+        cert.get_fingerprint("md5"),
+        cert.get_fingerprint("sha1"),
+        cert.get_issuer().as_text()
+    )
+
+
+# Command line arguments
+
+def list_clients(registry, name=None, verbose=False):
+    if name is not None:
+        client = registry.get_client(name)
+        if client is None:
+            print "No such client."
+            return
+        else:
+            clients = [client]
+    else:
+        clients = registry.get_clients()
+    for client in clients:
+        print(client)
+        if verbose:
+            print(client.verbose_str())
+        for cert in sorted(client.get_certs(), key=lambda c: c.get_not_after()):
+            print(format_cert(cert))
+            if verbose:
+                print(cert.as_text())
+
+
+def register_client(registry, name, admins=None, verbose=False):
+    try:
+        client = registry.new_client(name, admins or [])
+    except LookupError as e:
+        print(e)
+        return
+    client.save()
+    list_clients(registry, name, verbose)
+
+
+def applicant(registry, name, password=None, verbose=False):
+    client = registry.get_client(name)
+    if password is None:
+        password = "".join((random.choice(string.ascii_letters + string.digits) for dummy in range(16)))
+        print("Application password is: %s\n" % password)
+    client.allow_new_cert(pwd=password)
+    client.save()
+    list_clients(registry, name, verbose)
+
+
+def request(registry, key, csr, verbose=False):
+    openssl = subprocess.Popen(
+        [
+            "openssl", "req", "-new", "-nodes", "-batch",
+            "-keyout", key,
+            "-out", csr,
+            "-config", "/dev/stdin"
+        ], stdin=subprocess.PIPE
+    )
+    openssl.stdin.write(
+        "distinguished_name=req_distinguished_name\n"
+        "prompt=no\n"
+        "\n"
+        "[req_distinguished_name]\n"
+        "commonName=dummy"
+    )
+    openssl.stdin.close()
+    openssl.wait()
+    if verbose:
+        with open(csr, "r") as f:
+            print(f.read())
+
+
+def gen_cert(registry, name, csr, cert, password, verbose=False):
+    with open(csr, "r") as f:
+        csr_data = f.read()
+    client = registry.get_client(name)
+    newcert = client.new_cert(csr_data, password)
+    print(format_cert(newcert))
+    if verbose:
+        print(newcert.as_text())
+        print(newcert.as_pem())
+    with open(cert, "w") as f:
+        f.write(newcert.as_text())
+        f.write(newcert.as_pem())
+
+
+def get_args():
+    argp = argparse.ArgumentParser(
+        description="Warden server certificate registry", add_help=False)
+    argp.add_argument("--help", action="help",
+        help="show this help message and exit")
+    argp.add_argument("-c", "--config",
+        help="path to configuration file")
+    argp.add_argument("-v", "--verbose", action="store_true", default=False,
+        help="be more chatty")
+    subargp = argp.add_subparsers(title="commands")
+
+    subargp_list = subargp.add_parser("list", add_help=False,
+        description="List registered clients.",
+        help="list clients")
+    subargp_list.set_defaults(command=list_clients)
+    subargp_list.add_argument("--help", action="help",
+        help="show this help message and exit")
+    subargp_list.add_argument("--name", action="store", type=str,
+        help="client name")
+
+    subargp_reg = subargp.add_parser("register", add_help=False,
+        description="Add client registration entry.",
+        help="register client")
+    subargp_reg.set_defaults(command=register_client)
+    subargp_reg.add_argument("--help", action="help",
+        help="show this help message and exit")
+    subargp_reg.add_argument("--name", action="store", type=str,
+        required=True, help="client name")
+    subargp_reg.add_argument("--admins", action="store", type=str,
+        nargs="*", help="administrator list")
+
+    subargp_apply = subargp.add_parser("applicant", add_help=False,
+        description="Set client into certificate application mode and set its password",
+        help="allow for certificate application")
+    subargp_apply.set_defaults(command=applicant)
+    subargp_apply.add_argument("--help", action="help",
+        help="show this help message and exit")
+    subargp_apply.add_argument("--name", action="store", type=str,
+        required=True, help="client name")
+    subargp_apply.add_argument("--password", action="store", type=str,
+        help="password for application (will be autogenerated if not set)")
+
+    subargp_req = subargp.add_parser("request", add_help=False,
+        description="Generate certificate request",
+        help="generate CSR")
+    subargp_req.set_defaults(command=request)
+    subargp_req.add_argument("--help", action="help",
+        help="show this help message and exit")
+    subargp_req.add_argument("--key", action="store", type=str,
+        required=True, help="file for saving the key")
+    subargp_req.add_argument("--csr", action="store", type=str,
+        required=True, help="file for saving the request")
+
+    subargp_cert = subargp.add_parser("gencert", add_help=False,
+        description="Request new certificate from registry",
+        help="get new certificate")
+    subargp_cert.set_defaults(command=gen_cert)
+    subargp_cert.add_argument("--help", action="help",
+        help="show this help message and exit")
+    subargp_cert.add_argument("--name", action="store", type=str,
+        required=True, help="client name")
+    subargp_cert.add_argument("--csr", action="store", type=str,
+        required=True, help="file for saving the request")
+    subargp_cert.add_argument("--cert", action="store", type=str,
+        required=True, help="file for saving the new certificate")
+    subargp_cert.add_argument("--password", action="store", type=str,
+        required=True, help="password for application")
+
+    return argp.parse_args()
+
+
+def read_cfg(path):
+    with open(path, "r") as f:
+        stripcomments = "\n".join((l for l in f if not l.lstrip().startswith(("#", "//"))))
+        conf = json.loads(stripcomments)
+    return conf
+
+
+if __name__ == "__main__":
+    args = get_args()
+    config = read_cfg(os.path.join(os.path.dirname(__file__), args.config or "warden_ra.cfg"))
+    registry = EjbcaRegistry(**config)
+    if args.verbose:
+        print(registry)
+    command = args.command
+    subargs = vars(args)
+    del subargs["command"]
+    del subargs["config"]
+    sys.exit(command(registry, **subargs))
-- 
GitLab