mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +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):
|
def omit(**kwargs):
|
||||||
return dict([(key,value) for (key, value) in kwargs.items() if value])
|
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 authorize_admin(func):
|
||||||
def wrapped(self, req, resp, *args, **kwargs):
|
def wrapped(self, req, resp, *args, **kwargs):
|
||||||
authority = kwargs.get("ca")
|
authority = kwargs.get("ca")
|
||||||
@ -276,12 +285,12 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
raise falcon.HTTPConflict(
|
raise falcon.HTTPConflict(
|
||||||
"CSR with such CN already exists",
|
"CSR with such CN already exists",
|
||||||
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
"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
|
# Wait the certificate to be signed if waiting is requested
|
||||||
if req.get_param("wait"):
|
if req.get_param("wait"):
|
||||||
if ca.subscribe_certificate_url:
|
if ca.push_server:
|
||||||
# Redirect to nginx pub/sub
|
# 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)
|
click.echo("Redirecting to: %s" % url)
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.append_header("Location", url)
|
resp.append_header("Location", url)
|
||||||
@ -299,6 +308,8 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
# Request was accepted, but not processed
|
# Request was accepted, but not processed
|
||||||
resp.status = falcon.HTTP_202
|
resp.status = falcon.HTTP_202
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateStatusResource(CertificateAuthorityBase):
|
class CertificateStatusResource(CertificateAuthorityBase):
|
||||||
"""
|
"""
|
||||||
openssl ocsp -issuer CAcert_class1.pem -serial 0x<serial no in hex> -url http://localhost -CAfile cacert_both.pem
|
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
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
|
@event_source
|
||||||
@templatize("index.html")
|
@templatize("index.html")
|
||||||
def on_get(self, req, resp, ca, user):
|
def on_get(self, req, resp, ca, user):
|
||||||
return locals()
|
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
|
assert buf, "Server responded with no body, status code %d" % response.code
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||||
except crypto.Error:
|
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:
|
except urllib.error.HTTPError as e:
|
||||||
if e.code == 409:
|
if e.code == 409:
|
||||||
click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True)
|
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=Gentium" rel="stylesheet" type="text/css"/>
|
||||||
<link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" 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/jquery-2.1.4.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/certidude.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
@ -75,7 +76,7 @@ certidude setup openvpn client {{request.url}}
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for j in ca.get_requests() %}
|
{% 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>
|
<a class="button" href="/api/{{ca.slug}}/request/{{j.common_name}}/">Fetch</a>
|
||||||
{% if j.signable %}
|
{% if j.signable %}
|
||||||
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button>
|
<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>
|
<ul>
|
||||||
{% for j in ca.get_signed() | sort | reverse %}
|
{% 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>
|
<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>
|
<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>
|
<ul>
|
||||||
{% for j in ca.get_revoked() %}
|
{% for j in ca.get_revoked() %}
|
||||||
<li>
|
<li id="certificate_{{ j.fingerprint() }}">
|
||||||
{{j.changed}}
|
{{j.changed}}
|
||||||
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
|
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -28,14 +28,17 @@ def publish_certificate(func):
|
|||||||
cert = func(instance, csr, *args, **kwargs)
|
cert = func(instance, csr, *args, **kwargs)
|
||||||
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
||||||
|
|
||||||
if instance.publish_certificate_url:
|
if instance.push_server:
|
||||||
url = instance.publish_certificate_url % dict(request_sha1sum=csr.fingerprint())
|
url = instance.push_server + "/pub/?id=" + csr.fingerprint()
|
||||||
notification = urllib.request.Request(url, cert.dump().encode("ascii"))
|
notification = urllib.request.Request(url, cert.dump().encode("ascii"))
|
||||||
notification.add_header("User-Agent", "Certidude API")
|
notification.add_header("User-Agent", "Certidude API")
|
||||||
notification.add_header("Content-Type", "application/x-x509-user-cert")
|
notification.add_header("Content-Type", "application/x-x509-user-cert")
|
||||||
click.echo("Publishing certificate at %s, waiting for response..." % url)
|
click.echo("Publishing certificate at %s, waiting for response..." % url)
|
||||||
response = urllib.request.urlopen(notification)
|
response = urllib.request.urlopen(notification)
|
||||||
response.read()
|
response.read()
|
||||||
|
|
||||||
|
instance.event_publish("request_signed", csr.fingerprint())
|
||||||
|
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
# TODO: Implement e-mailing
|
# TODO: Implement e-mailing
|
||||||
@ -85,7 +88,7 @@ class CertificateAuthorityConfig(object):
|
|||||||
section = "CA_" + slug
|
section = "CA_" + slug
|
||||||
|
|
||||||
dirs = dict([(key, self.get(section, key))
|
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
|
# Variable expansion, eg $dir
|
||||||
for key, value in dirs.items():
|
for key, value in dirs.items():
|
||||||
@ -299,6 +302,7 @@ class CertificateBase:
|
|||||||
def fingerprint(self):
|
def fingerprint(self):
|
||||||
import binascii
|
import binascii
|
||||||
m, _ = self.pubkey
|
m, _ = self.pubkey
|
||||||
|
return "%x" % m
|
||||||
return ":".join(re.findall("..", hashlib.sha1(binascii.unhexlify("%x" % m)).hexdigest()))
|
return ":".join(re.findall("..", hashlib.sha1(binascii.unhexlify("%x" % m)).hexdigest()))
|
||||||
|
|
||||||
class Request(CertificateBase):
|
class Request(CertificateBase):
|
||||||
@ -424,7 +428,14 @@ class Certificate(CertificateBase):
|
|||||||
return self.signed <= other.signed
|
return self.signed <= other.signed
|
||||||
|
|
||||||
class CertificateAuthority(object):
|
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.slug = slug
|
||||||
self.revocation_list = crl
|
self.revocation_list = crl
|
||||||
self.signed_dir = certs
|
self.signed_dir = certs
|
||||||
@ -438,8 +449,7 @@ class CertificateAuthority(object):
|
|||||||
|
|
||||||
self.certificate = Certificate(open(certificate))
|
self.certificate = Certificate(open(certificate))
|
||||||
self.mailer = Mailer(outbox) if outbox else None
|
self.mailer = Mailer(outbox) if outbox else None
|
||||||
self.publish_certificate_url = publish_certificate_url
|
self.push_server = push_server
|
||||||
self.subscribe_certificate_url = subscribe_certificate_url
|
|
||||||
|
|
||||||
self.certificate_lifetime = certificate_lifetime
|
self.certificate_lifetime = certificate_lifetime
|
||||||
self.revocation_list_lifetime = revocation_list_lifetime
|
self.revocation_list_lifetime = revocation_list_lifetime
|
||||||
@ -459,6 +469,29 @@ class CertificateAuthority(object):
|
|||||||
else:
|
else:
|
||||||
self.admin_users = set([j for j in admin_users.split(" ") if j])
|
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):
|
def _signer_exec(self, cmd, *bits):
|
||||||
sock = self.connect_signer()
|
sock = self.connect_signer()
|
||||||
sock.send(cmd.encode("ascii"))
|
sock.send(cmd.encode("ascii"))
|
||||||
@ -486,6 +519,7 @@ class CertificateAuthority(object):
|
|||||||
cert = Certificate(open(os.path.join(self.signed_dir, cn + ".pem")))
|
cert = Certificate(open(os.path.join(self.signed_dir, cn + ".pem")))
|
||||||
revoked_filename = os.path.join(self.revoked_dir, "%s.pem" % cert.serial_number)
|
revoked_filename = os.path.join(self.revoked_dir, "%s.pem" % cert.serial_number)
|
||||||
os.rename(cert.path, revoked_filename)
|
os.rename(cert.path, revoked_filename)
|
||||||
|
self.event_publish("certificate_revoked", cert.fingerprint())
|
||||||
|
|
||||||
def get_revoked(self):
|
def get_revoked(self):
|
||||||
for root, dirs, files in os.walk(self.revoked_dir):
|
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"))
|
return os.path.exists(os.path.join(self.request_dir, cn + ".pem"))
|
||||||
|
|
||||||
def delete_request(self, cn):
|
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):
|
def create_bundle(self, common_name, organizational_unit=None, email_address=None, overwrite=True):
|
||||||
req = Request.create()
|
req = Request.create()
|
||||||
|
Loading…
Reference in New Issue
Block a user