mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Added preliminary event handling for front-end
This commit is contained in:
parent
f1c0a3925d
commit
a413a15854
@ -21,6 +21,15 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0
|
||||
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):
|
||||
if req.get_header("Accept") == "text/event-stream":
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.location = ca.push_server + "/ev/" + ca.uuid
|
||||
print("Delegating EventSource handling to:", resp.location)
|
||||
return func(self, req, resp, ca, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
def authorize_admin(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
authority = kwargs.get("ca")
|
||||
@ -276,12 +285,12 @@ class RequestListResource(CertificateAuthorityBase):
|
||||
raise falcon.HTTPConflict(
|
||||
"CSR with such CN already exists",
|
||||
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
||||
|
||||
ca.event_publish("request_submitted", request.fingerprint())
|
||||
# Wait the certificate to be signed if waiting is requested
|
||||
if req.get_param("wait"):
|
||||
if ca.subscribe_certificate_url:
|
||||
if ca.push_server:
|
||||
# Redirect to nginx pub/sub
|
||||
url = ca.subscribe_certificate_url % dict(request_sha1sum=request.fingerprint())
|
||||
url = ca.push_server + "/lp/" + request.fingerprint()
|
||||
click.echo("Redirecting to: %s" % url)
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.append_header("Location", url)
|
||||
@ -299,6 +308,8 @@ class RequestListResource(CertificateAuthorityBase):
|
||||
# Request was accepted, but not processed
|
||||
resp.status = falcon.HTTP_202
|
||||
|
||||
|
||||
|
||||
class CertificateStatusResource(CertificateAuthorityBase):
|
||||
"""
|
||||
openssl ocsp -issuer CAcert_class1.pem -serial 0x<serial no in hex> -url http://localhost -CAfile cacert_both.pem
|
||||
@ -322,6 +333,7 @@ class IndexResource(CertificateAuthorityBase):
|
||||
@login_required
|
||||
@pop_certificate_authority
|
||||
@authorize_admin
|
||||
@event_source
|
||||
@templatize("index.html")
|
||||
def on_get(self, req, resp, ca, user):
|
||||
return locals()
|
||||
|
@ -1,60 +0,0 @@
|
||||
|
||||
import click
|
||||
import falcon
|
||||
import kerberos
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
|
||||
# Vanilla Kerberos provides only username.
|
||||
# AD also embeds PAC (Privilege Attribute Certificate), which
|
||||
# is supposed to be sent via HTTP headers and it contains
|
||||
# the groups user is part of.
|
||||
# Even then we would have to manually look up the e-mail
|
||||
# address eg via LDAP, hence to keep things simple
|
||||
# we simply use Kerberos to authenticate.
|
||||
|
||||
FQDN = socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3]
|
||||
|
||||
if not os.getenv("KRB5_KTNAME"):
|
||||
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
|
||||
exit(250)
|
||||
|
||||
try:
|
||||
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
|
||||
except kerberos.KrbError as exc:
|
||||
click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True)
|
||||
exit(249)
|
||||
else:
|
||||
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
|
||||
|
||||
def login_required(func):
|
||||
def wrapped(resource, req, resp, *args, **kwargs):
|
||||
authorization = req.get_header("Authorization")
|
||||
|
||||
if not authorization:
|
||||
resp.append_header("WWW-Authenticate", "Negotiate")
|
||||
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?")
|
||||
|
||||
token = ''.join(authorization.split()[1:])
|
||||
|
||||
rc, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
|
||||
if rc != kerberos.AUTH_GSS_COMPLETE:
|
||||
raise falcon.HTTPForbidden("Forbidden", "Kerberos ticket expired?")
|
||||
|
||||
rc = kerberos.authGSSServerStep(context, token)
|
||||
kerberos_user = kerberos.authGSSServerUserName(context).split("@")
|
||||
|
||||
# References lost beyond this point! Results in
|
||||
# ValueError: PyCapsule_SetPointer called with null pointer
|
||||
kerberos.authGSSServerClean(context)
|
||||
|
||||
if rc == kerberos.AUTH_GSS_COMPLETE:
|
||||
kwargs["user"] = kerberos_user
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
elif rc == kerberos.AUTH_GSS_CONTINUE:
|
||||
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
|
||||
else:
|
||||
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
|
||||
|
||||
return wrapped
|
@ -155,7 +155,10 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
assert buf, "Server responded with no body, status code %d" % response.code
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
if buf == b'-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n':
|
||||
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
|
||||
else:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 409:
|
||||
click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True)
|
||||
|
30
certidude/static/js/certidude.js
Normal file
30
certidude/static/js/certidude.js
Normal file
@ -0,0 +1,30 @@
|
||||
$(document).ready(function() {
|
||||
console.info("Opening EventSource from:", window.location.href);
|
||||
|
||||
var source = new EventSource(window.location.href);
|
||||
|
||||
source.onmessage = function(event) {
|
||||
console.log("Received server-sent event:", event);
|
||||
}
|
||||
|
||||
source.addEventListener("request_deleted", function(e) {
|
||||
console.log("Removing deleted request #" + e.data);
|
||||
$("#request_" + e.data).remove();
|
||||
});
|
||||
|
||||
source.addEventListener("request_submitted", function(e) {
|
||||
console.log("Request submitted:", e.data);
|
||||
});
|
||||
|
||||
source.addEventListener("request_signed", function(e) {
|
||||
console.log("Request signed:", e.data);
|
||||
$("#request_" + e.data).remove();
|
||||
// TODO: Insert <li> to signed certs list
|
||||
});
|
||||
|
||||
source.addEventListener("certificate_revoked", function(e) {
|
||||
console.log("Removing revoked certificate #" + e.data);
|
||||
$("#certificate_" + e.data).remove();
|
||||
});
|
||||
|
||||
});
|
@ -9,6 +9,7 @@
|
||||
<link href="//fonts.googleapis.com/css?family=Gentium" rel="stylesheet" type="text/css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" rel="stylesheet" type="text/css"/>
|
||||
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
|
||||
<script type="text/javascript" src="/js/certidude.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -75,7 +76,7 @@ certidude setup openvpn client {{request.url}}
|
||||
|
||||
<ul>
|
||||
{% for j in ca.get_requests() %}
|
||||
<li>
|
||||
<li id="request_{{ j.fingerprint() }}">
|
||||
<a class="button" href="/api/{{ca.slug}}/request/{{j.common_name}}/">Fetch</a>
|
||||
{% if j.signable %}
|
||||
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button>
|
||||
@ -129,7 +130,7 @@ curl -f {{request.url}}/signed/$CN > $CN.crt
|
||||
|
||||
<ul>
|
||||
{% for j in ca.get_signed() | sort | reverse %}
|
||||
<li>
|
||||
<li id="certificate_{{ j.fingerprint() }}">
|
||||
<a class="button" href="/api/{{ca.slug}}/signed/{{j.subject.CN}}/">Fetch</a>
|
||||
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
||||
|
||||
@ -177,7 +178,7 @@ openssl ocsp -issuer ca.pem -CAfile ca.pem -url {{request.url}}/ocsp/ -serial 0x
|
||||
-->
|
||||
<ul>
|
||||
{% for j in ca.get_revoked() %}
|
||||
<li>
|
||||
<li id="certificate_{{ j.fingerprint() }}">
|
||||
{{j.changed}}
|
||||
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
|
||||
</li>
|
||||
|
@ -28,14 +28,17 @@ def publish_certificate(func):
|
||||
cert = func(instance, csr, *args, **kwargs)
|
||||
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
||||
|
||||
if instance.publish_certificate_url:
|
||||
url = instance.publish_certificate_url % dict(request_sha1sum=csr.fingerprint())
|
||||
if instance.push_server:
|
||||
url = instance.push_server + "/pub/?id=" + csr.fingerprint()
|
||||
notification = urllib.request.Request(url, cert.dump().encode("ascii"))
|
||||
notification.add_header("User-Agent", "Certidude API")
|
||||
notification.add_header("Content-Type", "application/x-x509-user-cert")
|
||||
click.echo("Publishing certificate at %s, waiting for response..." % url)
|
||||
response = urllib.request.urlopen(notification)
|
||||
response.read()
|
||||
|
||||
instance.event_publish("request_signed", csr.fingerprint())
|
||||
|
||||
return cert
|
||||
|
||||
# TODO: Implement e-mailing
|
||||
@ -85,7 +88,7 @@ class CertificateAuthorityConfig(object):
|
||||
section = "CA_" + slug
|
||||
|
||||
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", "publish_certificate_url", "subscribe_certificate_url", "inbox", "outbox")])
|
||||
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", "inbox", "outbox")])
|
||||
|
||||
# Variable expansion, eg $dir
|
||||
for key, value in dirs.items():
|
||||
@ -299,6 +302,7 @@ class CertificateBase:
|
||||
def fingerprint(self):
|
||||
import binascii
|
||||
m, _ = self.pubkey
|
||||
return "%x" % m
|
||||
return ":".join(re.findall("..", hashlib.sha1(binascii.unhexlify("%x" % m)).hexdigest()))
|
||||
|
||||
class Request(CertificateBase):
|
||||
@ -424,7 +428,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, publish_certificate_url=None, subscribe_certificate_url=None):
|
||||
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):
|
||||
|
||||
import hashlib
|
||||
m = hashlib.sha512()
|
||||
m.update(slug.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
|
||||
@ -438,8 +449,7 @@ class CertificateAuthority(object):
|
||||
|
||||
self.certificate = Certificate(open(certificate))
|
||||
self.mailer = Mailer(outbox) if outbox else None
|
||||
self.publish_certificate_url = publish_certificate_url
|
||||
self.subscribe_certificate_url = subscribe_certificate_url
|
||||
self.push_server = push_server
|
||||
|
||||
self.certificate_lifetime = certificate_lifetime
|
||||
self.revocation_list_lifetime = revocation_list_lifetime
|
||||
@ -459,6 +469,29 @@ class CertificateAuthority(object):
|
||||
else:
|
||||
self.admin_users = set([j for j in admin_users.split(" ") if j])
|
||||
|
||||
def event_publish(self, event_type, event_data):
|
||||
"""
|
||||
Publish event on push server
|
||||
"""
|
||||
url = self.push_server + "/pub?id=" + self.uuid # Derive CA's push channel URL
|
||||
|
||||
notification = urllib.request.Request(
|
||||
url,
|
||||
event_data.encode("utf-8"),
|
||||
{"Event-ID": b"TODO", "Event-Type":event_type.encode("ascii")})
|
||||
notification.add_header("User-Agent", "Certidude API")
|
||||
click.echo("Posting event %s %s at %s, waiting for response..." % (repr(event_type), repr(event_data), repr(url)))
|
||||
try:
|
||||
response = urllib.request.urlopen(notification)
|
||||
body = response.read()
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == 404:
|
||||
print("No subscribers on the channel")
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
print("Push server returned:", response.code, body)
|
||||
|
||||
def _signer_exec(self, cmd, *bits):
|
||||
sock = self.connect_signer()
|
||||
sock.send(cmd.encode("ascii"))
|
||||
@ -486,6 +519,7 @@ class CertificateAuthority(object):
|
||||
cert = Certificate(open(os.path.join(self.signed_dir, cn + ".pem")))
|
||||
revoked_filename = os.path.join(self.revoked_dir, "%s.pem" % cert.serial_number)
|
||||
os.rename(cert.path, revoked_filename)
|
||||
self.event_publish("certificate_revoked", cert.fingerprint())
|
||||
|
||||
def get_revoked(self):
|
||||
for root, dirs, files in os.walk(self.revoked_dir):
|
||||
@ -532,7 +566,28 @@ class CertificateAuthority(object):
|
||||
return os.path.exists(os.path.join(self.request_dir, cn + ".pem"))
|
||||
|
||||
def delete_request(self, cn):
|
||||
os.unlink(os.path.join(self.request_dir, cn + ".pem"))
|
||||
path = os.path.join(self.request_dir, cn + ".pem")
|
||||
request_sha1sum = Request(open(path)).fingerprint()
|
||||
os.unlink(path)
|
||||
|
||||
# Publish event at CA channel
|
||||
self.event_publish("request_deleted", request_sha1sum)
|
||||
|
||||
# Write empty certificate to long-polling URL
|
||||
url = self.push_server + "/pub/?id=" + request_sha1sum
|
||||
publisher = urllib.request.Request(url, b"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n")
|
||||
publisher.add_header("User-Agent", "Certidude API")
|
||||
click.echo("POST-ing empty certificate at %s, waiting for response..." % url)
|
||||
try:
|
||||
response = urllib.request.urlopen(publisher)
|
||||
body = response.read()
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code == 404:
|
||||
print("No subscribers on the channel")
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
print("Push server returned:", response.code, body)
|
||||
|
||||
def create_bundle(self, common_name, organizational_unit=None, email_address=None, overwrite=True):
|
||||
req = Request.create()
|
||||
|
Loading…
Reference in New Issue
Block a user