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\nMails will be sent to: ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape);
-output += "
\n\nYou 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 += "\nYou 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\nAuthority certificate
\n\nSeveral 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\nMails 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\nAuthority settings
\n\nThese can be reconfigured via /etc/certidude/server.conf on the server.
\n\nOutgoing mail server:\n";
+output += "
\n\n\nAuthority settings
\n\nThese can be reconfigured via /etc/certidude/server.conf on the server.
\n\nUser 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\nOutgoing 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 }}