From 7012f5b365b67fec6efce3efc0e32994ec2deff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Fri, 1 Apr 2016 01:55:51 +0300 Subject: [PATCH] Make user certificate enrollment configurable --- certidude/api/__init__.py | 21 ++++++----------- certidude/api/bundle.py | 34 +++++++++++++++++++++++++++ certidude/authority.py | 10 ++++++-- certidude/config.py | 6 +++++ certidude/static/js/templates.js | 27 +++++++++++++++++++-- certidude/static/views/authority.html | 15 ++++++++++++ certidude/templates/certidude.conf | 10 ++++++++ 7 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 certidude/api/bundle.py diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 52a5f71..1d82d64 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -56,6 +56,8 @@ class SessionResource(object): [req.context.get("remote_addr") in j for j in config.REQUEST_SUBNETS]), authority = dict( + user_certificate_enrollment=config.USER_CERTIFICATE_ENROLLMENT, + user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES, outbox = config.OUTBOX, certificate = authority.certificate, events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, @@ -103,18 +105,6 @@ class StaticResource(object): resp.status = falcon.HTTP_404 resp.body = "File '%s' not found" % req.path - -class BundleResource(object): - @login_required - def on_get(self, req, resp): - common_name = req.context["user"].mail - logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user")) - resp.set_header("Content-Type", "application/x-pkcs12") - resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) - resp.body, cert = authority.generate_pkcs12_bundle(common_name, - owner=req.context.get("user")) - - import ipaddress class NormalizeMiddleware(object): @@ -129,7 +119,7 @@ class NormalizeMiddleware(object): def certidude_app(): from certidude import config - + from .bundle import BundleResource from .revoked import RevocationListResource from .signed import SignedCertificateListResource, SignedCertificateDetailResource from .request import RequestListResource, RequestDetailResource @@ -143,7 +133,6 @@ def certidude_app(): # Certificate authority API calls app.add_route("/api/ocsp/", CertificateStatusResource()) - app.add_route("/api/bundle/", BundleResource()) app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/revoked/", RevocationListResource()) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) @@ -156,6 +145,10 @@ def certidude_app(): app.add_route("/api/lease/", LeaseResource()) app.add_route("/api/whois/", WhoisResource()) + # Optional user enrollment API call + if config.USER_CERTIFICATE_ENROLLMENT: + app.add_route("/api/bundle/", BundleResource()) + log_handlers = [] if config.LOGGING_BACKEND == "sql": from certidude.mysqllog import LogHandler diff --git a/certidude/api/bundle.py b/certidude/api/bundle.py new file mode 100644 index 0000000..6a1a637 --- /dev/null +++ b/certidude/api/bundle.py @@ -0,0 +1,34 @@ + +import logging +import hashlib +from certidude import config, authority +from certidude.auth import login_required + +logger = logging.getLogger("api") + +KEYWORDS = ( + ("Android", "android"), + ("iPhone", "iphone"), + ("iPad", "ipad"), + ("Ubuntu", "ubuntu"), +) + +class BundleResource(object): + @login_required + def on_get(self, req, resp): + common_name = req.context["user"].name + if config.USER_MULTIPLE_CERTIFICATES: + for key, value in KEYWORDS: + if key in req.user_agent: + device_identifier = value + break + else: + device_identifier = "unknown-device" + common_name += "@" + device_identifier + "-" + \ + hashlib.sha256(req.user_agent).hexdigest()[:8] + logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user")) + resp.set_header("Content-Type", "application/x-pkcs12") + resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) + resp.body, cert = authority.generate_pkcs12_bundle(common_name, + owner=req.context.get("user")) + diff --git a/certidude/authority.py b/certidude/authority.py index 2e51bee..e24137e 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -26,10 +26,16 @@ def publish_certificate(func): cert = func(csr, *args, **kwargs) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) + if cert.given_name and cert.surname and cert.email_address: + recipient = "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) + elif cert.email_address: + recipient = cert.email_address + else: + recipient = None + mailer.send( "certificate-signed.md", - to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if - cert.given_name and cert.surname else cert.email_address, + to=recipient, attachments=(cert,), certificate=cert) diff --git a/certidude/config.py b/certidude/config.py index 63aedac..f369d7c 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -39,6 +39,12 @@ SIGNED_DIR = cp.get("authority", "signed dir") REVOKED_DIR = cp.get("authority", "revoked dir") OUTBOX = cp.get("authority", "outbox") +USER_CERTIFICATE_ENROLLMENT = { + "forbidden": False, "single allowed": True, "multiple allowed": True }[ + cp.get("authority", "user certificate enrollment")] +USER_MULTIPLE_CERTIFICATES = { + "forbidden": False, "single allowed": False, "multiple allowed": True }[ + cp.get("authority", "user certificate enrollment")] CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment" diff --git a/certidude/static/js/templates.js b/certidude/static/js/templates.js index ae55796..d0901db 100644 --- a/certidude/static/js/templates.js +++ b/certidude/static/js/templates.js @@ -469,11 +469,34 @@ output += " ("; output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"name"), env.opts.autoescape); output += ") settings\n\n

Mails will be sent to: "; output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape); -output += "

\n\n

You can click here to generate bundle\nfor current user account.

\n\n"; +output += "

\n\n"; +if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_certificate_enrollment")) { +output += "\n

You can click here to generate bundle\nfor current user account.

\n"; +; +} +output += "\n\n"; if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { output += "\n\n

Authority certificate

\n\n

Several things such as CRL location and e-mails are hardcoded into\nthe certificate and\nas such require complete reset of X509 infrastructure if some of them needs to be changed:

\n\n

Mails will appear from: "; output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"certificate")),"email_address"), env.opts.autoescape); -output += "

\n\n\n

Authority settings

\n\n

These can be reconfigured via /etc/certidude/server.conf on the server.

\n\n

Outgoing mail server:\n"; +output += "

\n\n\n

Authority settings

\n\n

These can be reconfigured via /etc/certidude/server.conf on the server.

\n\n

User certificate enrollment:\n"; +if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_certificate_enrollment")) { +output += "\n "; +if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_mutliple_certificates")) { +output += "\n multiple\n "; +; +} +else { +output += "\n single\n "; +; +} +output += "\nallowed\n"; +; +} +else { +output += "\nforbidden\n"; +; +} +output += "\n

\n\n

Outgoing mail server:\n"; if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox")) { output += "\n "; output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox"), env.opts.autoescape); diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 019f480..1b1b5c6 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -4,8 +4,10 @@

Mails will be sent to: {{ session.user.mail }}

+{% if session.authority.user_certificate_enrollment %}

You can click here to generate bundle for current user account.

+{% endif %} {% if session.authority %} @@ -22,6 +24,19 @@ as such require complete reset of X509 infrastructure if some of them needs to b

These can be reconfigured via /etc/certidude/server.conf on the server.

+

User certificate enrollment: +{% if session.authority.user_certificate_enrollment %} + {% if session.authority.user_mutliple_certificates %} + multiple + {% else %} + single + {% endif %} +allowed +{% else %} +forbidden +{% endif %} +

+

Outgoing mail server: {% if session.authority.outbox %} {{ session.authority.outbox }} diff --git a/certidude/templates/certidude.conf b/certidude/templates/certidude.conf index 0e765c8..4188e97 100644 --- a/certidude/templates/certidude.conf +++ b/certidude/templates/certidude.conf @@ -71,6 +71,16 @@ long poll = {{ push_server }}/lp/%s publish = {{ push_server }}/pub?id=%s [authority] +# User certificate enrollment specifies whether logged in users are allowed to +# request bundles. In case of 'single allowed' the common name of the +# certificate is set to username, this should work well with REMOTE_USER +# enabled web apps running behind Apache/nginx. +# In case of 'multiple allowed' the common name is set to username@device-identifier. +;user certificate enrollment = forbidden +;user certificate enrollment = single allowed +user certificate enrollment = multiple allowed + + private key path = {{ ca_key }} certificate path = {{ ca_crt }}