From f89358233874f547f0eecbf7753d7e9ee0132095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Sun, 15 Nov 2015 15:55:26 +0100 Subject: [PATCH] Major refactoring, CA is associated with it's hostname now --- README.rst | 38 ++- certidude/api.py | 251 +++++++++++------- certidude/auth.py | 16 +- certidude/cli.py | 79 +++--- certidude/helpers.py | 7 +- certidude/signer.py | 14 +- certidude/static/authority.html | 51 +--- certidude/static/css/style.css | 25 +- certidude/static/error.html | 2 + .../img/iconmonstr-magnifier-4-icon.svg | 27 ++ certidude/static/index.html | 1 + certidude/static/js/certidude.js | 51 +++- certidude/static/request.html | 26 +- certidude/static/signed.html | 31 +++ certidude/templates/__init__.py | 0 .../iconmonstr-certificate-15-icon.svg | 35 --- .../templates/iconmonstr-email-2-icon.svg | 21 -- .../templates/iconmonstr-flag-3-icon.svg | 11 - certidude/templates/iconmonstr-key-2-icon.svg | 15 -- .../templates/iconmonstr-time-13-icon.svg | 18 -- certidude/templates/index.html | 199 -------------- certidude/templates/openssl.cnf | 13 +- certidude/wrappers.py | 26 +- 23 files changed, 424 insertions(+), 533 deletions(-) create mode 100644 certidude/static/error.html create mode 100644 certidude/static/img/iconmonstr-magnifier-4-icon.svg create mode 100644 certidude/static/signed.html delete mode 100644 certidude/templates/__init__.py delete mode 100644 certidude/templates/iconmonstr-certificate-15-icon.svg delete mode 100644 certidude/templates/iconmonstr-email-2-icon.svg delete mode 100644 certidude/templates/iconmonstr-flag-3-icon.svg delete mode 100644 certidude/templates/iconmonstr-key-2-icon.svg delete mode 100644 certidude/templates/iconmonstr-time-13-icon.svg delete mode 100644 certidude/templates/index.html diff --git a/README.rst b/README.rst index 125bae9..0b4873e 100644 --- a/README.rst +++ b/README.rst @@ -83,11 +83,20 @@ Create a system user for ``certidude``: Setting up CA -------------- -Certidude can set up CA relatively easily: +First make sure the machine used for CA has fully qualified +domain name set up properly. +You can check it with: + + hostname -f + +The command should return ca.example.co + +Certidude can set up CA relatively easily, following will set up +CA in /var/lib/certidude/hostname.domain: .. code:: bash - certidude setup authority /path/to/directory + certidude setup authority Tweak command-line options until you meet your requirements and then insert generated section to your /etc/ssl/openssl.cnf @@ -112,7 +121,7 @@ Use following command to request a certificate on a machine: .. code:: - certidude setup client http://certidude-hostname-or-ip:perhaps-port/api/ca-name/ + certidude setup client ca.example.com Use following to list signing requests, certificates and revoked certificates: @@ -185,20 +194,26 @@ configure the site in /etc/nginx/sites-available.d/certidude: listen 80 default_server; listen [::]:80 default_server ipv6only=on; - location ~ /event/publish/(.*) { - allow 127.0.0.1; # Allow publishing only from this IP address + location /pub { + allow 127.0.0.1; # Allow publishing only from CA machine push_stream_publisher admin; - push_stream_channels_path $1; + push_stream_channels_path $arg_id; } - location ~ /event/subscribe/(.*) { + location ~ "^/lp/(.*)" { push_stream_channels_path $1; push_stream_subscriber long-polling; } + location ~ "^/ev/(.*)" { + push_stream_channels_path $1; + push_stream_subscriber eventsource; + } + location / { - include uwsgi_params; - uwsgi_pass certidude_api; + proxy_pass http://ca.koodur.com/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; } } @@ -239,8 +254,7 @@ Also adjust ``/etc/nginx/nginx.conf``: In your CA ssl.cnf make sure Certidude is aware of your nginx setup: - publish_certificate_url = http://push.example.com/event/publish/%(request_sha1sum)s - subscribe_certificate_url = http://push.example.com/event/subscribe/%(request_sha1sum)s + push_server = http://push.example.com/ Restart the services: @@ -338,7 +352,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content: case "$2" in up) - LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager http://ca.example.org/api/laptops/ gateway.example.org + LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager ca.example.com gateway.example.com ;; esac diff --git a/certidude/api.py b/certidude/api.py index 104570f..f1c6cbb 100644 --- a/certidude/api.py +++ b/certidude/api.py @@ -21,22 +21,45 @@ env = Environment(loader=PackageLoader("certidude", "templates")) 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])$" + +OIDS = { + (2, 5, 4, 3) : 'CN', # common name + (2, 5, 4, 6) : 'C', # country + (2, 5, 4, 7) : 'L', # locality + (2, 5, 4, 8) : 'ST', # stateOrProvince + (2, 5, 4, 10) : 'O', # organization + (2, 5, 4, 11) : 'OU', # organizationalUnit +} + +def parse_dn(data): + chunks, remainder = decoder.decode(data) + dn = "" + if remainder: + raise ValueError() + # TODO: Check for duplicate entries? + def generate(): + for chunk in chunks: + for chunkette in chunk: + key, value = chunkette + yield str(OIDS[key] + "=" + value) + return ", ".join(generate()) + def omit(**kwargs): return dict([(key,value) for (key, value) in kwargs.items() if value]) def event_source(func): - def wrapped(self, req, resp, ca, *args, **kwargs): + def wrapped(self, req, resp, *args, **kwargs): if req.get_header("Accept") == "text/event-stream": resp.status = falcon.HTTP_SEE_OTHER - resp.location = ca.push_server + "/ev/" + ca.uuid + resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid resp.body = "Redirecting to:" + resp.location print("Delegating EventSource handling to:", resp.location) - return func(self, req, resp, ca, *args, **kwargs) + return func(self, req, resp, *args, **kwargs) return wrapped def authorize_admin(func): def wrapped(self, req, resp, *args, **kwargs): - authority = kwargs.get("ca") + authority = req.context.get("ca") # Parse remote IPv4/IPv6 address remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) @@ -50,19 +73,19 @@ def authorize_admin(func): raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) # Check for username whitelist - kerberos_username, kerberos_realm = kwargs.get("user") + kerberos_username, kerberos_realm = req.context.get("user") if kerberos_username not in authority.admin_users: raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % kerberos_username) # Retain username, TODO: Better abstraction with username, e-mail, sn, gn? - kwargs["user"] = kerberos_username + return func(self, req, resp, *args, **kwargs) return wrapped def pop_certificate_authority(func): def wrapped(self, req, resp, *args, **kwargs): - kwargs["ca"] = self.config.instantiate_authority(kwargs["ca"]) + req.context["ca"] = self.config.instantiate_authority(req.env["HTTP_HOST"]) return func(self, req, resp, *args, **kwargs) return wrapped @@ -111,7 +134,7 @@ class MyEncoder(json.JSONEncoder): if isinstance(obj, CertificateAuthority): return dict( event_channel = obj.push_server + "/ev/" + obj.uuid, - slug = obj.slug, + common_name = obj.common_name, certificate = obj.certificate, admin_users = obj.admin_users, autosign_subnets = obj.autosign_subnets, @@ -137,12 +160,12 @@ def serialize(func): resp.set_header("Expires", "0"); r = func(instance, req, resp, **kwargs) if resp.body is None: - if not req.client_accepts_json: - raise falcon.HTTPUnsupportedMediaType( - "This API only supports the JSON media type.", - href="http://docs.examples.com/api/json") - resp.set_header("Content-Type", "application/json") - resp.body = json.dumps(r, cls=MyEncoder) + if req.get_header("Accept").split(",")[0] == "application/json": + resp.set_header("Content-Type", "application/json") + resp.append_header("Content-Disposition", "inline") + resp.body = json.dumps(r, cls=MyEncoder) + else: + resp.body = repr(r) return r return wrapped @@ -162,7 +185,6 @@ def templatize(path): resp.set_header("Content-Type", "application/json") r.pop("req") r.pop("resp") - r.pop("user") resp.body = json.dumps(r, cls=MyEncoder) return r else: @@ -180,59 +202,39 @@ class CertificateAuthorityBase(object): class RevocationListResource(CertificateAuthorityBase): @pop_certificate_authority - def on_get(self, req, resp, ca): + def on_get(self, req, resp): resp.set_header("Content-Type", "application/x-pkcs7-crl") - resp.append_header("Content-Disposition", "attachment; filename=%s.crl" % ca.slug) - resp.body = ca.export_crl() + resp.append_header("Content-Disposition", "attachment; filename=%s.crl" % req.context.get("ca").common_name) + resp.body = req.context.get("ca").export_crl() class SignedCertificateDetailResource(CertificateAuthorityBase): + @serialize @pop_certificate_authority @validate_common_name - def on_get(self, req, resp, ca, cn): - path = os.path.join(ca.signed_dir, cn + ".pem") + def on_get(self, req, resp, cn): + path = os.path.join(req.context.get("ca").signed_dir, cn + ".pem") if not os.path.exists(path): raise falcon.HTTPNotFound() - resp.stream = open(path, "rb") + resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % cn) + return Certificate(open(path)) @login_required @pop_certificate_authority @authorize_admin @validate_common_name - def on_delete(self, req, resp, ca, cn, user): - ca.revoke(cn) + def on_delete(self, req, resp, cn): + req.context.get("ca").revoke(cn) class LeaseResource(CertificateAuthorityBase): @serialize @login_required @pop_certificate_authority @authorize_admin - def on_get(self, req, resp, ca, user): + def on_get(self, req, resp): from ipaddress import ip_address - OIDS = { - (2, 5, 4, 3) : 'CN', # common name - (2, 5, 4, 6) : 'C', # country - (2, 5, 4, 7) : 'L', # locality - (2, 5, 4, 8) : 'ST', # stateOrProvince - (2, 5, 4, 10) : 'O', # organization - (2, 5, 4, 11) : 'OU', # organizationalUnit - } - - def parse_dn(data): - chunks, remainder = decoder.decode(data) - dn = "" - if remainder: - raise ValueError() - # TODO: Check for duplicate entries? - def generate(): - for chunk in chunks: - for chunkette in chunk: - key, value = chunkette - yield str(OIDS[key] + "=" + value) - return ", ".join(generate()) - # BUGBUG SQL_LEASES = """ SELECT @@ -249,17 +251,18 @@ class LeaseResource(CertificateAuthorityBase): WHERE addresses.released <> 1 """ - cnx = ca.database.get_connection() - cursor = cnx.cursor(dictionary=True) + cnx = req.context.get("ca").database.get_connection() + cursor = cnx.cursor() query = (SQL_LEASES) cursor.execute(query) - for row in cursor: - row["acquired"] = datetime.utcfromtimestamp(row["acquired"]) - row["released"] = datetime.utcfromtimestamp(row["released"]) if row["released"] else None - row["address"] = ip_address(bytes(row["address"])) - row["identity"] = parse_dn(bytes(row["identity"])) - yield row + for acquired, released, address, identity in cursor: + yield { + "acquired": datetime.utcfromtimestamp(acquired), + "released": datetime.utcfromtimestamp(released) if released else None, + "address": ip_address(bytes(address)), + "identity": parse_dn(bytes(identity)) + } class SignedCertificateListResource(CertificateAuthorityBase): @@ -267,7 +270,7 @@ class SignedCertificateListResource(CertificateAuthorityBase): @pop_certificate_authority @authorize_admin @validate_common_name - def on_get(self, req, resp, ca): + def on_get(self, req, resp): for j in authority.get_signed(): yield omit( key_type=j.key_type, @@ -283,29 +286,31 @@ class SignedCertificateListResource(CertificateAuthorityBase): class RequestDetailResource(CertificateAuthorityBase): + @serialize @pop_certificate_authority @validate_common_name - def on_get(self, req, resp, ca, cn): + def on_get(self, req, resp, cn): """ Fetch certificate signing request as PEM """ - path = os.path.join(ca.request_dir, cn + ".pem") + path = os.path.join(req.context.get("ca").request_dir, cn + ".pem") if not os.path.exists(path): raise falcon.HTTPNotFound() - resp.stream = open(path, "rb") + resp.append_header("Content-Type", "application/x-x509-user-cert") resp.append_header("Content-Disposition", "attachment; filename=%s.csr" % cn) + return Request(open(path)) @login_required @pop_certificate_authority @authorize_admin @validate_common_name - def on_patch(self, req, resp, ca, cn, user): + def on_patch(self, req, resp, cn): """ Sign a certificate signing request """ - csr = ca.get_request(cn) - cert = ca.sign(csr, overwrite=True, delete=True) + csr = req.context.get("ca").get_request(cn) + cert = req.context.get("ca").sign(csr, overwrite=True, delete=True) os.unlink(csr.path) resp.body = "Certificate successfully signed" resp.status = falcon.HTTP_201 @@ -314,15 +319,16 @@ class RequestDetailResource(CertificateAuthorityBase): @login_required @pop_certificate_authority @authorize_admin - def on_delete(self, req, resp, ca, cn, user): - ca.delete_request(cn) + def on_delete(self, req, resp, cn): + req.context.get("ca").delete_request(cn) + class RequestListResource(CertificateAuthorityBase): @serialize @pop_certificate_authority @authorize_admin - def on_get(self, req, resp, ca): - for j in ca.get_requests(): + def on_get(self, req, resp): + for j in req.context.get("ca").get_requests(): yield omit( key_type=j.key_type, key_length=j.key_length, @@ -336,12 +342,13 @@ class RequestListResource(CertificateAuthorityBase): fingerprint=j.fingerprint()) @pop_certificate_authority - def on_post(self, req, resp, ca): + def on_post(self, req, resp): """ Submit certificate signing request (CSR) in PEM format """ # Parse remote IPv4/IPv6 address remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) + ca = req.context.get("ca") # Check for CSR submission whitelist if ca.request_subnets: @@ -415,12 +422,11 @@ class RequestListResource(CertificateAuthorityBase): resp.status = falcon.HTTP_202 - class CertificateStatusResource(CertificateAuthorityBase): """ openssl ocsp -issuer CAcert_class1.pem -serial 0x -url http://localhost -CAfile cacert_both.pem """ - def on_post(self, req, resp, ca): + def on_post(self, req, resp): ocsp_request = req.stream.read(req.content_length) for component in decoder.decode(ocsp_request): click.echo(component) @@ -430,10 +436,10 @@ class CertificateStatusResource(CertificateAuthorityBase): class CertificateAuthorityResource(CertificateAuthorityBase): @pop_certificate_authority - def on_get(self, req, resp, ca): - path = os.path.join(ca.certificate.path) + def on_get(self, req, resp): + path = os.path.join(req.context.get("ca").certificate.path) resp.stream = open(path, "rb") - resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug) + resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % req.context.get("ca").common_name) class IndexResource(CertificateAuthorityBase): @serialize @@ -441,45 +447,95 @@ class IndexResource(CertificateAuthorityBase): @pop_certificate_authority @authorize_admin @event_source - def on_get(self, req, resp, ca, user): - return ca + def on_get(self, req, resp): + return req.context.get("ca") -class AuthorityListResource(CertificateAuthorityBase): +class SessionResource(CertificateAuthorityBase): @serialize @login_required - def on_get(self, req, resp, user): + def on_get(self, req, resp): return dict( authorities=(self.config.ca_list), # TODO: Check if user is CA admin - username=user[0] + username=req.context.get("user")[0] ) +def address_to_identity(cnx, addr): + """ + Translate currently online client's IP-address to distinguished name + """ + + SQL_LEASES = """ + SELECT + acquired, + released, + identities.data as identity + FROM + addresses + RIGHT JOIN + identities + ON + identities.id = addresses.identity + WHERE + address = %s AND + released IS NOT NULL + """ + + cursor = cnx.cursor() + query = (SQL_LEASES) + import struct + cursor.execute(query, (struct.pack("!L", int(addr)),)) + + for acquired, released, identity in cursor: + return { + "acquired": datetime.utcfromtimestamp(acquired), + "identity": parse_dn(bytes(identity)) + } + return None + + +class WhoisResource(CertificateAuthorityBase): + @serialize + @pop_certificate_authority + def on_get(self, req, resp): + identity = address_to_identity( + req.context.get("ca").database.get_connection(), + ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) + ) + + if identity: + return identity + else: + resp.status = falcon.HTTP_403 + resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"] + + class ApplicationConfigurationResource(CertificateAuthorityBase): @pop_certificate_authority @validate_common_name - def on_get(self, req, resp, ca, cn): + def on_get(self, req, resp, cn): ctx = dict( cn = cn, - certificate = ca.get_certificate(cn), - ca_certificate = open(ca.certificate.path, "r").read()) + certificate = req.context.get("ca").get_certificate(cn), + ca_certificate = open(req.context.get("ca").certificate.path, "r").read()) resp.append_header("Content-Type", "application/ovpn") resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn) - resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx) + resp.body = Template(open("/etc/openvpn/%s.template" % req.context.get("ca").common_name).read()).render(ctx) @login_required @pop_certificate_authority @authorize_admin @validate_common_name - def on_put(self, req, resp, user, ca, cn=None): - pkey_buf, req_buf, cert_buf = ca.create_bundle(cn) + def on_put(self, req, resp, cn=None): + pkey_buf, req_buf, cert_buf = req.context.get("ca").create_bundle(cn) ctx = dict( private_key = pkey_buf, certificate = cert_buf, - ca_certificate = ca.certificate.dump()) + ca_certificate = req.context.get("ca").certificate.dump()) resp.append_header("Content-Type", "application/ovpn") resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn) - resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx) + resp.body = Template(open("/etc/openvpn/%s.template" % req.context.get("ca").common_name).read()).render(ctx) class StaticResource(object): @@ -513,15 +569,20 @@ def certidude_app(): config = CertificateAuthorityConfig() app = falcon.API() - app.add_route("/api/ca/{ca}/ocsp/", CertificateStatusResource(config)) - app.add_route("/api/ca/{ca}/signed/{cn}/openvpn", ApplicationConfigurationResource(config)) - app.add_route("/api/ca/{ca}/certificate/", CertificateAuthorityResource(config)) - app.add_route("/api/ca/{ca}/revoked/", RevocationListResource(config)) - app.add_route("/api/ca/{ca}/signed/{cn}/", SignedCertificateDetailResource(config)) - app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config)) - app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config)) - app.add_route("/api/ca/{ca}/request/", RequestListResource(config)) - app.add_route("/api/ca/{ca}/lease/", LeaseResource(config)) - app.add_route("/api/ca/{ca}/", IndexResource(config)) - app.add_route("/api/ca/", AuthorityListResource(config)) + + # Certificate authority API calls + app.add_route("/api/ocsp/", CertificateStatusResource(config)) + app.add_route("/api/signed/{cn}/openvpn", ApplicationConfigurationResource(config)) + app.add_route("/api/certificate/", CertificateAuthorityResource(config)) + app.add_route("/api/revoked/", RevocationListResource(config)) + app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(config)) + app.add_route("/api/signed/", SignedCertificateListResource(config)) + app.add_route("/api/request/{cn}/", RequestDetailResource(config)) + app.add_route("/api/request/", RequestListResource(config)) + app.add_route("/api/", IndexResource(config)) + app.add_route("/api/session/", SessionResource(config)) + + # Gateway API calls, should this be moved to separate project? + app.add_route("/api/lease/", LeaseResource(config)) + app.add_route("/api/whois/", WhoisResource(config)) return app diff --git a/certidude/auth.py b/certidude/auth.py index 7956d7e..670535a 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -34,35 +34,35 @@ def login_required(func): if not authorization: resp.append_header("WWW-Authenticate", "Negotiate") - raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?") + raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") token = ''.join(authorization.split()[1:]) try: result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) except kerberos.GSSError as ex: - raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex[0][0], ex[1][0],)) + raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],)) try: result = kerberos.authGSSServerStep(context, token) except kerberos.GSSError as ex: + s = str(dir(ex)) kerberos.authGSSServerClean(context) - raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s(%s)" % (ex[0][0], ex[1][0],)) + raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%s)" % (ex.args[0][0], ex.args[1][0])) except kerberos.KrbError as ex: kerberos.authGSSServerClean(context) - raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex[0],)) + raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],)) - kerberos_user = kerberos.authGSSServerUserName(context).split("@") + req.context["user"] = kerberos.authGSSServerUserName(context).split("@") try: # BUGBUG: https://github.com/02strich/pykerberos/issues/6 #kerberos.authGSSServerClean(context) pass except kerberos.GSSError as ex: - raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex[0][0], ex[1][0],)) - + raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],)) + if result == kerberos.AUTH_GSS_COMPLETE: - kwargs["user"] = kerberos_user return func(resource, req, resp, *args, **kwargs) elif result == kerberos.AUTH_GSS_CONTINUE: raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") diff --git a/certidude/cli.py b/certidude/cli.py index 9dde6d8..5c5e6f0 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -52,7 +52,7 @@ SURNAME = None EMAIL = None if USERNAME: - EMAIL = USERNAME + "@" + HOSTNAME + EMAIL = USERNAME + "@" + FQDN if os.getuid() >= 1000: _, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME) @@ -106,14 +106,14 @@ def certidude_spawn(kill, no_interaction): ca_loaded = False config = load_config() for ca in config.all_authorities(): - socket_path = os.path.join(signer_dir, ca.slug + ".sock") - pidfile_path = os.path.join(signer_dir, ca.slug + ".pid") + socket_path = os.path.join(signer_dir, ca.common_name + ".sock") + pidfile_path = os.path.join(signer_dir, ca.common_name + ".pid") try: with open(pidfile_path) as fh: pid = int(fh.readline()) os.kill(pid, 0) - click.echo("Found process with PID %d for %s" % (pid, ca.slug)) + click.echo("Found process with PID %d for %s" % (pid, ca.common_name)) except (ValueError, ProcessLookupError, FileNotFoundError): pid = 0 @@ -138,9 +138,9 @@ def certidude_spawn(kill, no_interaction): with open(pidfile_path, "w") as fh: fh.write("%d\n" % os.getpid()) - setproctitle("%s spawn %s" % (sys.argv[0], ca.slug)) + setproctitle("%s spawn %s" % (sys.argv[0], ca.common_name)) logging.basicConfig( - filename="/var/log/certidude-%s.log" % ca.slug, + filename="/var/log/certidude-%s.log" % ca.common_name, level=logging.INFO) server = SignServer(socket_path, ca.private_key, ca.certificate.path, ca.certificate_lifetime, ca.basic_constraints, ca.key_usage, @@ -524,15 +524,15 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw @click.command("authority", help="Set up Certificate Authority in a directory") @click.option("--parent", "-p", help="Parent CA, none by default") -@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, hostname by default") -@click.option("--country", "-c", default="ee", help="Country, Estonia by default") -@click.option("--state", "-s", default="Harjumaa", help="State or country, Harjumaa by default") -@click.option("--locality", "-l", default="Tallinn", help="City or locality, Tallinn by default") +@click.option("--common-name", "-cn", default=FQDN, help="Common name, fully qualified hostname by default") +@click.option("--country", "-c", default=None, help="Country, none by default") +@click.option("--state", "-s", default=None, help="State or country, none by default") +@click.option("--locality", "-l", default=None, help="City or locality, none by default") @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 days (20 years) by default") @click.option("--certificate-lifetime", default=5*365, help="Certificate lifetime in days, 1825 days (5 years) by default") @click.option("--revocation-list-lifetime", default=1, help="Revocation list lifetime in days, 1 day by default") -@click.option("--organization", "-o", default="Example LLC", help="Company or organization name") -@click.option("--organizational-unit", "-ou", default="Certification Department") +@click.option("--organization", "-o", default=None, help="Company or organization name") +@click.option("--organizational-unit", "-ou", default=None) @click.option("--pkcs11", default=False, is_flag=True, help="Use PKCS#11 token instead of files") @click.option("--crl-distribution-url", default=None, help="CRL distribution URL") @click.option("--ocsp-responder-url", default=None, help="OCSP responder URL") @@ -540,17 +540,14 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw @click.option("--inbox", default="imap://user:pass@host:port/INBOX", help="Inbound e-mail server") @click.option("--outbox", default="smtp://localhost", help="Outbound e-mail server") @click.option("--push-server", default="", help="Streaming nginx push server") -@click.argument("directory") +@click.option("--directory", default=None, help="Directory for authority files, /var/lib/certidude// by default") def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, email_address, inbox, outbox, push_server): - publish_certificate_url = push_server + "/publish/%(request_sha1sum)s" - subscribe_certificate_url = push_server + "/subscribe/%(request_sha1sum)s" + if not directory: + directory = os.path.join("/var/lib/certidude", common_name) - slug = os.path.basename(directory[:-1] if directory.endswith('/') else directory) - if not slug: - raise click.ClickException("Please supply proper target path") - # Make sure slug is valid - if not re.match(r"^[_a-zA-Z0-9]+$", slug): + # Make sure common_name is valid + if not re.match(r"^[\._a-zA-Z0-9]+$", common_name): raise click.ClickException("CA name can contain only alphanumeric and '_' characters") if os.path.lexists(directory): @@ -567,7 +564,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or key.generate_key(crypto.TYPE_RSA, 4096) if not crl_distribution_url: - crl_distribution_url = "http://%s/api/%s/revoked/" % (common_name, slug) + crl_distribution_url = "http://%s/api/revoked/" % common_name # File paths ca_key = os.path.join(directory, "ca_key.pem") @@ -579,11 +576,18 @@ def certidude_setup_authority(parent, country, state, locality, organization, or ca.set_version(2) # This corresponds to X.509v3 ca.set_serial_number(1) ca.get_subject().CN = common_name - ca.get_subject().C = country - ca.get_subject().ST = state - ca.get_subject().L = locality - ca.get_subject().O = organization - ca.get_subject().OU = organizational_unit + + if country: + ca.get_subject().C = country + if state: + ca.get_subject().ST = state + if locality: + ca.get_subject().L = locality + if organization: + ca.get_subject().O = organization + if organizational_unit: + ca.get_subject().OU = organizational_unit + ca.gmtime_adj_notBefore(0) ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) ca.set_issuer(ca.get_subject()) @@ -621,7 +625,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or raise NotImplementedError() """ - ocsp_responder_url = "http://%s/api/%s/ocsp/" % (common_name, slug) + ocsp_responder_url = "http://%s/api/ocsp/" % common_name authority_info_access = "OCSP;URI:%s" % ocsp_responder_url ca.add_extensions([ crypto.X509Extension( @@ -661,18 +665,19 @@ def certidude_setup_authority(parent, country, state, locality, organization, or with open(ca_key, "wb") as fh: fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) - with open(os.path.join(directory, "openssl.cnf.example"), "w") as fh: + ssl_cnf_example = os.path.join(directory, "openssl.cnf.example") + with open(ssl_cnf_example, "w") as fh: fh.write(env.get_template("openssl.cnf").render(locals())) - click.echo("You need to copy the contents of the 'openssl.cnf.example'") + click.echo("You need to copy the contents of the '%s'" % ssl_cnf_example) click.echo("to system-wide OpenSSL configuration file, usually located") click.echo("at /etc/ssl/openssl.cnf") click.echo() click.echo("Use following commands to inspect the newly created files:") click.echo() - click.echo(" openssl crl -inform PEM -text -noout -in %s" % ca_crl) - click.echo(" openssl x509 -text -noout -in %s" % ca_crt) + click.echo(" openssl crl -inform PEM -text -noout -in %s | less" % ca_crl) + click.echo(" openssl x509 -text -noout -in %s | less" % ca_crt) click.echo(" openssl rsa -check -in %s" % ca_key) click.echo(" openssl verify -CAfile %s %s" % (ca_crt, ca_crt)) click.echo() @@ -746,7 +751,7 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ if not hide_requests: for j in ca.get_requests(): if not verbose: - click.echo("s " + j.path + " " + j.distinguished_name) + click.echo("s " + j.path + " " + j.identity) continue click.echo(click.style(j.common_name, fg="blue")) click.echo("=" * len(j.common_name)) @@ -777,11 +782,11 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ for j in ca.get_signed(): if not verbose: if j.signed < NOW and j.expires > NOW: - click.echo("v " + j.path + " " + j.distinguished_name) + click.echo("v " + j.path + " " + j.identity) elif NOW > j.expires: - click.echo("e " + j.path + " " + j.distinguished_name) + click.echo("e " + j.path + " " + j.identity) else: - click.echo("y " + j.path + " " + j.distinguished_name) + click.echo("y " + j.path + " " + j.identity) continue click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white")) @@ -803,7 +808,7 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ if show_revoked: for j in ca.get_revoked(): if not verbose: - click.echo("r " + j.path + " " + j.distinguished_name) + click.echo("r " + j.path + " " + j.identity) continue click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white")) click.echo("="*(len(j.common_name)+60)) @@ -869,7 +874,7 @@ def certidude_sign(common_name, overwrite, lifetime): # Sign directly using private key cert = ca.sign2(request, overwrite, True, lifetime) - click.echo("Signed %s" % cert.distinguished_name) + click.echo("Signed %s" % cert.identity) for key, value, data in cert.extensions: click.echo("Added extension %s: %s" % (key, value)) click.echo() diff --git a/certidude/helpers.py b/certidude/helpers.py index 2ad3b59..700f945 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -40,10 +40,15 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, # Set up URL-s request_params = set() if autosign: - request_params.add("autosign=yes") + request_params.add("autosign=true") if wait: request_params.add("wait=forever") + # Expand ca.example.com to http://ca.example.com/api/ + if not "/" in url: + url += "/api/" + if "//" not in url: + url = "http://" + url if not url.endswith("/"): url = url + "/" diff --git a/certidude/signer.py b/certidude/signer.py index 797614f..9bc688e 100644 --- a/certidude/signer.py +++ b/certidude/signer.py @@ -50,10 +50,14 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa # raise ValueError("Country mismatch!") # Copy attributes from CA - cert.get_subject().C = ca_cert.get_subject().C - cert.get_subject().ST = ca_cert.get_subject().ST - cert.get_subject().L = ca_cert.get_subject().L - cert.get_subject().O = ca_cert.get_subject().O + if ca_cert.get_subject().C: + cert.get_subject().C = ca_cert.get_subject().C + if ca_cert.get_subject().ST: + cert.get_subject().ST = ca_cert.get_subject().ST + if ca_cert.get_subject().L: + cert.get_subject().L = ca_cert.get_subject().L + if ca_cert.get_subject().O: + cert.get_subject().O = ca_cert.get_subject().O # Copy attributes from request cert.get_subject().CN = request.get_subject().CN @@ -198,7 +202,7 @@ class SignServer(asyncore.dispatcher): # Dropping privileges _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") - os.chroot("/run/certidude/signer/jail") + #os.chroot("/run/certidude/signer/jail") os.setgid(gid) os.setuid(uid) diff --git a/certidude/static/authority.html b/certidude/static/authority.html index fc181c6..1f671f2 100644 --- a/certidude/static/authority.html +++ b/certidude/static/authority.html @@ -1,4 +1,4 @@ -

{{authority.slug}} management

+

{{authority.common_name}} management

Hi {{session.username}},

@@ -9,51 +9,26 @@ {% set s = authority.certificate.identity %} + + +

Pending requests

-
    - {% for j in authority.requests %} +
      + {% for request in authority.requests %} {% include "request.html" %} - {% else %} -
    • Great job! No certificate signing requests to sign.
    • {% endfor %} +
    • +

      No certificate signing requests to sign! You can submit a certificate signing request by:

      +
      certidude setup client {{authority.common_name}}
      +

    Signed certificates

      - {% for j in authority.signed | sort | reverse %} -
    • - Fetch - - -
      - {% include 'img/iconmonstr-certificate-15-icon.svg' %} - {{j.identity}} -
      - - {% if j.email_address %} - - {% endif %} - -
      - {% include 'img/iconmonstr-key-2-icon.svg' %} - - {{ j.sha256sum }} - - {{ j.key_length }}-bit - {{ j.key_type }} -
      - -
      - {% include 'img/iconmonstr-flag-3-icon.svg' %} - {{j.key_usage}} -
      - -
      - {% include 'status.html' %} -
      -
    • + {% for certificate in authority.signed | sort | reverse %} + {% include "signed.html" %} {% endfor %}
    @@ -61,7 +36,7 @@

    To fetch certificate revocation list:

    -curl {{request.url}}/revoked/ | openssl crl -text -noout
    +curl {{window.location.href}}api/revoked/ | openssl crl -text -noout
     
    + + + + + + + + + + diff --git a/certidude/static/index.html b/certidude/static/index.html index 50bbf5a..afb0ab9 100644 --- a/certidude/static/index.html +++ b/certidude/static/index.html @@ -8,6 +8,7 @@ +
    diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 29b336a..ae3cda5 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -3,8 +3,16 @@ $(document).ready(function() { $.ajax({ method: "GET", - url: "/api/ca/", + url: "/api/session/", dataType: "json", + error: function(response) { + if (response.responseJSON) { + var msg = response.responseJSON + } else { + var msg = { title: "Error " + response.status, description: response.statusText } + } + $("#container").html(nunjucks.render('error.html', { message: msg })); + }, success: function(session, status, xhr) { console.info("Loaded CA list:", session); @@ -15,7 +23,7 @@ $(document).ready(function() { $.ajax({ method: "GET", - url: "/api/ca/" + session.authorities[0], + url: "/api/", dataType: "json", success: function(authority, status, xhr) { console.info("Got CA:", authority); @@ -61,12 +69,33 @@ $(document).ready(function() { source.addEventListener("request_submitted", function(e) { console.log("Request submitted:", e.data); + $.ajax({ + method: "GET", + url: "/api/request/lauri-c720p/", + dataType: "json", + success: function(request, status, xhr) { + console.info(request); + $("#pending_requests").prepend( + nunjucks.render('request.html', { request: request })); + } + }); + }); source.addEventListener("request_signed", function(e) { console.log("Request signed:", e.data); $("#request_" + e.data).slideUp("normal", function() { $(this).remove(); }); - // TODO: Insert
  • to signed certs list + + $.ajax({ + method: "GET", + url: "/api/signed/lauri-c720p/", + dataType: "json", + success: function(certificate, status, xhr) { + console.info(certificate); + $("#signed_certificates").prepend( + nunjucks.render('signed.html', { certificate: certificate })); + } + }); }); source.addEventListener("certificate_revoked", function(e) { @@ -74,11 +103,11 @@ $(document).ready(function() { $("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); }); }); - $("#container").html(nunjucks.render('authority.html', { authority: authority, session: session })); + $("#container").html(nunjucks.render('authority.html', { authority: authority, session: session, window: window })); $.ajax({ method: "GET", - url: "/api/ca/" + authority.slug + "/lease/", + url: "/api/lease/", dataType: "json", success: function(leases, status, xhr) { console.info("Got leases:", leases); @@ -96,6 +125,18 @@ $(document).ready(function() { released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null }})); } + + /* Set up search box */ + $("#search").on("keyup", function() { + var q = $("#search").val().toLowerCase(); + $(".filterable").each(function(i, e) { + if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) { + $(e).show(); + } else { + $(e).hide(); + } + }); + }); } }); } diff --git a/certidude/static/request.html b/certidude/static/request.html index 98faa07..02d1be5 100644 --- a/certidude/static/request.html +++ b/certidude/static/request.html @@ -1,37 +1,37 @@ -
  • +
  • -Fetch -{% if j.signable %} - +Fetch +{% if request.signable %} + {% else %} {% endif %} - +
    {% include 'img/iconmonstr-certificate-15-icon.svg' %} -{{j.identity}} +{{request.identity}}
    -{% if j.email_address %} - +{% if request.email_address %} + {% endif %}
    {% include 'img/iconmonstr-key-2-icon.svg' %} -{{ j.sha256sum }} +{{ request.sha256sum }} -{{ j.key_length }}-bit -{{ j.key_type }} +{{ request.key_length }}-bit +{{ request.key_type }}
    -{% set key_usage = j.key_usage %} +{% set key_usage = request.key_usage %} {% if key_usage %}
    {% include 'img/iconmonstr-flag-3-icon.svg' %} -{{j.key_usage}} +{{request.key_usage}}
    {% endif %} diff --git a/certidude/static/signed.html b/certidude/static/signed.html new file mode 100644 index 0000000..85ba3e6 --- /dev/null +++ b/certidude/static/signed.html @@ -0,0 +1,31 @@ +
  • + Fetch + + +
    + {% include 'img/iconmonstr-certificate-15-icon.svg' %} + {{certificate.identity}} +
    + + {% if certificate.email_address %} + + {% endif %} + +
    + {% include 'img/iconmonstr-key-2-icon.svg' %} + + {{ certificate.sha256sum }} + + {{ certificate.key_length }}-bit + {{ certificate.key_type }} +
    + +
    + {% include 'img/iconmonstr-flag-3-icon.svg' %} + {{certificate.key_usage}} +
    + +
    + {% include 'status.html' %} +
    +
  • diff --git a/certidude/templates/__init__.py b/certidude/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/certidude/templates/iconmonstr-certificate-15-icon.svg b/certidude/templates/iconmonstr-certificate-15-icon.svg deleted file mode 100644 index 8b27cec..0000000 --- a/certidude/templates/iconmonstr-certificate-15-icon.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/certidude/templates/iconmonstr-email-2-icon.svg b/certidude/templates/iconmonstr-email-2-icon.svg deleted file mode 100644 index 258086e..0000000 --- a/certidude/templates/iconmonstr-email-2-icon.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/templates/iconmonstr-flag-3-icon.svg b/certidude/templates/iconmonstr-flag-3-icon.svg deleted file mode 100644 index 8e12498..0000000 --- a/certidude/templates/iconmonstr-flag-3-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/certidude/templates/iconmonstr-key-2-icon.svg b/certidude/templates/iconmonstr-key-2-icon.svg deleted file mode 100644 index 1301a5c..0000000 --- a/certidude/templates/iconmonstr-key-2-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/certidude/templates/iconmonstr-time-13-icon.svg b/certidude/templates/iconmonstr-time-13-icon.svg deleted file mode 100644 index 189521b..0000000 --- a/certidude/templates/iconmonstr-time-13-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - diff --git a/certidude/templates/index.html b/certidude/templates/index.html deleted file mode 100644 index eeef271..0000000 --- a/certidude/templates/index.html +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - Certidude server - - - - - - - - -
    - -

    Submit signing request

    - -

    Hi, {{user}}

    - -

    Request submission is allowed from: {% if ca.request_subnets %}{% for i in ca.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}

    -

    Autosign is allowed from: {% if ca.autosign_subnets %}{% for i in ca.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}

    -

    Authority administration is allowed from: {% if ca.admin_subnets %}{% for i in ca.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %} -

    Authority administration allowed for: {% for i in ca.admin_users %}{{ i }} {% endfor %}

    - -

    IPsec gateway on OpenWrt

    - -{% set s = ca.certificate.subject %} - -
    -opkg update
    -opkg install strongswan-default curl openssl-util
    -modprobe authenc
    -
    - -

    Generate key and submit using standard shell tools:

    - -
    -CN=$(cat /proc/sys/kernel/hostname)
    -curl {{request.url}}/certificate/ > /etc/ipsec.d/cacerts/ca.pem
    -openssl genrsa -out /etc/ipsec.d/private/$CN.pem 4096
    -chmod 0600 /etc/ipsec.d/private/$CN.pem
    -openssl req -new -sha256 -key /etc/ipsec.d/private/$CN.pem -out /etc/ipsec.d/reqs/$CN.pem -subj "{% if s.C %}/C={{s.C}}{% endif %}{% if s.ST %}/ST={{s.ST}}{% endif %}{% if s.L %}/L={{s.L}}{% endif %}{% if s.O %}/O={{s.O}}{% endif %}{% if s.OU %}/OU={{s.OU}}{% endif %}/CN=$CN"
    -curl -L -H "Content-Type: application/pkcs10" --data-binary @/etc/ipsec.d/reqs/$CN.pem {{request.uri}}/request/?autosign=yes\&wait=30 > /etc/ipsec.d/certs/$CN.pem.part
    -if [ $? -eq 0 ]; then mv /etc/ipsec.d/certs/$CN.pem.part /etc/ipsec.d/certs/$CN.pem; fi
    -openssl verify -CAfile /etc/ipsec.d/cacerts/ca.pem /etc/ipsec.d/certs/$CN.pem
    -
    - -

    -Inspect newly created files: -

    - -
    -openssl x509 -text -noout -in /etc/ipsec.d/cacerts/ca.pem
    -openssl x509 -text -noout -in /etc/ipsec.d/certs/$CN.pem
    -openssl rsa -check -in /etc/ipsec.d/private/$CN.pem
    -
    - -

    Assuming you have Certidude installed

    - -
    -certidude setup client {{request.url}}
    -
    - -

    To set up OpenVPN server

    -
    -certidude setup openvpn server {{request.url}}
    -
    - -

    Or to set up OpenVPN client

    -
    -certidude setup openvpn client {{request.url}}
    -
    - -

    Pending requests

    - -
      - {% for j in ca.get_requests() %} -
    • - Fetch - {% if j.signable %} - - {% else %} - - {% endif %} - - - -
      - {% include 'iconmonstr-certificate-15-icon.svg' %} - {{j.distinguished_name}} -
      - - {% if j.email_address %} - - {% endif %} - -
      - {% include 'iconmonstr-key-2-icon.svg' %} - - {{ j.fingerprint() }} - - {{ j.key_length }}-bit - {{ j.key_type }} -
      - - {% set key_usage = j.key_usage %} - {% if key_usage %} -
      - {% include 'iconmonstr-flag-3-icon.svg' %} - {{j.key_usage}} -
      - {% endif %} - -
    • - {% else %} -
    • Great job! No certificate signing requests to sign.
    • - {% endfor %} -
    - -

    Signed certificates

    - - - -

    You can fetch a certificate by common name signing the request

    - -
    -curl -f {{request.url}}/signed/$CN > $CN.crt
    -
    - -
      - {% for j in ca.get_signed() | sort | reverse %} -
    • - Fetch - - -
      - {% include 'iconmonstr-certificate-15-icon.svg' %} - {{j.distinguished_name}} -
      - - {% if j.email_address %} - - {% endif %} - -
      - {% include 'iconmonstr-key-2-icon.svg' %} - - {{ j.fingerprint() }} - - {{ j.key_length }}-bit - {{ j.key_type }} -
      - -
      - {% include 'iconmonstr-flag-3-icon.svg' %} - {{j.key_usage}} -
      - - -
    • - {% endfor %} -
    - -

    Revoked certificates

    - -

    To fetch certificate revocation list:

    -
    -curl {{request.url}}/revoked/ | openssl crl -text -noout
    -
    - -
      - {% for j in ca.get_revoked() %} -
    • - {{j.changed}} - {{j.serial_number}} {{j.distinguished_name}} -
    • - {% else %} -
    • Great job! No certificate signing requests to sign.
    • - {% endfor %} -
    - -
    - - - - - - diff --git a/certidude/templates/openssl.cnf b/certidude/templates/openssl.cnf index a1524f8..d0839bd 100644 --- a/certidude/templates/openssl.cnf +++ b/certidude/templates/openssl.cnf @@ -1,7 +1,7 @@ # You have to copy the settings to the system-wide # OpenSSL configuration (usually /etc/ssl/openssl.cnf -[CA_{{slug}}] +[CA_{{common_name}}] default_crl_days = {{revocation_list_lifetime}} default_days = {{certificate_lifetime}} dir = {{directory}} @@ -18,8 +18,8 @@ crlDistributionPoints = {{crl_distribution_points}} {% if email_address %} emailAddress = {{email_address}} {% endif %} -x509_extensions = {{slug}}_cert -policy = policy_{{slug}} +x509_extensions = {{common_name}}_cert +policy = policy_{{common_name}} # Certidude specific stuff, TODO: move to separate section? request_subnets = 10.0.0.0/8 192.168.0.0/16 172.168.0.0/16 @@ -28,10 +28,9 @@ admin_subnets = 127.0.0.0/8 admin_users = inbox = {{inbox}} outbox = {{outbox}} -publish_certificate_url = {{publish_certificate_url}} -subscribe_certificate_url = {{subscribe_certificate_url}} +push_server = {{push_server}} -[policy_{{slug}}] +[policy_{{common_name}}] countryName = match stateOrProvinceName = match organizationName = match @@ -39,7 +38,7 @@ organizationalUnitName = optional commonName = supplied emailAddress = optional -[{{slug}}_cert] +[{{common_name}}_cert] basicConstraints = CA:FALSE keyUsage = nonRepudiation,digitalSignature,keyEncipherment extendedKeyUsage = clientAuth diff --git a/certidude/wrappers.py b/certidude/wrappers.py index b7278b2..ea8403d 100644 --- a/certidude/wrappers.py +++ b/certidude/wrappers.py @@ -84,8 +84,8 @@ class CertificateAuthorityConfig(object): else: return default - def instantiate_authority(self, slug): - section = "CA_" + slug + def instantiate_authority(self, common_name): + section = "CA_" + common_name dirs = dict([(key, self.get(section, key)) for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_subnets", "autosign_subnets", "admin_subnets", "admin_users", "push_server", "database", "inbox", "outbox")]) @@ -105,7 +105,7 @@ class CertificateAuthorityConfig(object): dirs["basic_constraints"] = self.get(extensions_section, "basicConstraints") dirs["key_usage"] = self.get(extensions_section, "keyUsage") dirs["extended_key_usage"] = self.get(extensions_section, "extendedKeyUsage") - authority = CertificateAuthority(slug, **dirs) + authority = CertificateAuthority(common_name, **dirs) return authority @@ -129,13 +129,16 @@ class CertificateAuthorityConfig(object): def pop_certificate_authority(self): def wrapper(func): def wrapped(*args, **kwargs): - slug = kwargs.pop("ca") - kwargs["ca"] = self.instantiate_authority(slug) + common_name = kwargs.pop("ca") + kwargs["ca"] = self.instantiate_authority(common_name) return func(*args, **kwargs) return wrapped return wrapper class CertificateBase: + def __repr__(self): + return self.buf + @property def given_name(self): return self.subject.GN @@ -433,15 +436,14 @@ class Certificate(CertificateBase): return self.signed <= other.signed class CertificateAuthority(object): - def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None, database=None): + def __init__(self, common_name, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None, database=None): import hashlib m = hashlib.sha512() - m.update(slug.encode("ascii")) + m.update(common_name.encode("ascii")) m.update(b"TODO:server-secret-goes-here") self.uuid = m.hexdigest() - self.slug = slug self.revocation_list = crl self.signed_dir = certs self.request_dir = new_certs_dir @@ -476,6 +478,10 @@ class CertificateAuthority(object): else: self.admin_users = set([j for j in admin_users.split(" ") if j]) + @property + def common_name(self): + return self.certificate.common_name + @property def database(self): from urllib.parse import urlparse @@ -530,14 +536,14 @@ class CertificateAuthority(object): return buf def __repr__(self): - return "CertificateAuthority(slug=%s)" % repr(self.slug) + return "CertificateAuthority(common_name=%s)" % repr(self.common_name) def get_certificate(self, cn): return open(os.path.join(self.signed_dir, cn + ".pem")).read() def connect_signer(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect("/run/certidude/signer/%s.sock" % self.slug) + sock.connect("/run/certidude/signer/%s.sock" % self.common_name) return sock def revoke(self, cn):