api: Added signed certificate tagging mechanism

This commit is contained in:
Lauri Võsandi 2015-12-16 17:41:49 +00:00
parent 901b0f7224
commit da6600e2e9
21 changed files with 501 additions and 143 deletions

View File

@ -79,6 +79,7 @@ def certidude_app():
from .lease import LeaseResource from .lease import LeaseResource
from .whois import WhoisResource from .whois import WhoisResource
from .log import LogResource from .log import LogResource
from .tag import TagResource, TagDetailResource
app = falcon.API() app = falcon.API()
@ -91,6 +92,8 @@ def certidude_app():
app.add_route("/api/request/{cn}/", RequestDetailResource()) app.add_route("/api/request/{cn}/", RequestDetailResource())
app.add_route("/api/request/", RequestListResource()) app.add_route("/api/request/", RequestListResource())
app.add_route("/api/log/", LogResource()) app.add_route("/api/log/", LogResource())
app.add_route("/api/tag/", TagResource())
app.add_route("/api/tag/{identifier}/", TagDetailResource())
app.add_route("/api/", SessionResource()) app.add_route("/api/", SessionResource())
# Gateway API calls, should this be moved to separate project? # Gateway API calls, should this be moved to separate project?
@ -128,12 +131,13 @@ def certidude_app():
logger.addHandler(push_handler) logger.addHandler(push_handler)
logging.getLogger("cli").info("Started Certidude at %s", socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3]) logging.getLogger("cli").debug("Started Certidude at %s",
socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3])
import atexit import atexit
def exit_handler(): def exit_handler():
logging.getLogger("cli").info("Shutting down Certidude") logging.getLogger("cli").debug("Shutting down Certidude")
atexit.register(exit_handler) atexit.register(exit_handler)

View File

@ -73,7 +73,7 @@ class RequestListResource(object):
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")
push.publish("request_submitted", csr.common_name) push.publish("request-submitted", csr.common_name)
# 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"):

View File

@ -1,9 +1,12 @@
import falcon import falcon
import logging
from certidude import authority from certidude import authority
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize
logger = logging.getLogger("api")
class SignedCertificateListResource(object): class SignedCertificateListResource(object):
@serialize @serialize
@authorize_admin @authorize_admin
@ -26,13 +29,17 @@ class SignedCertificateDetailResource(object):
@serialize @serialize
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
try: try:
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
return authority.get_signed(cn) return authority.get_signed(cn)
except FileNotFoundError: except FileNotFoundError:
logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.body = "No certificate CN=%s found" % cn resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
@login_required @login_required
@authorize_admin @authorize_admin
def on_delete(self, req, resp, cn): def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"])
authority.revoke_certificate(cn) authority.revoke_certificate(cn)

90
certidude/api/tag.py Normal file
View File

@ -0,0 +1,90 @@
import falcon
import logging
from certidude import config
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
logger = logging.getLogger("api")
class TagResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("select * from tag")
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return tuple(g())
@serialize
@login_required
@authorize_admin
def on_post(self, req, resp):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
args = req.get_param("cn"), req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`cn`, `key`, `value`) values (%s, %s, %s)", args)
push.publish("tag-added", str(cursor.lastrowid))
logger.debug("Tag cn=%s, key=%s, value=%s added" % args)
conn.commit()
cursor.close()
conn.close()
class TagDetailResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp, identifier):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("select * from tag where `id` = %s", (identifier,))
for row in cursor:
cursor.close()
conn.close()
return row
cursor.close()
conn.close()
raise falcon.HTTPNotFound()
@serialize
@login_required
@authorize_admin
def on_put(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("update tag set `value` = %s where `id` = %s limit 1",
(req.get_param("value"), identifier))
conn.commit()
cursor.close()
conn.close()
logger.debug("Tag %s updated, value set to %s",
identifier, req.get_param("value"))
push.publish("tag-updated", identifier)
@serialize
@login_required
@authorize_admin
def on_delete(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("delete from tag where tag.id = %s", (identifier,))
conn.commit()
cursor.close()
conn.close()
push.publish("tag-removed", identifier)
logger.debug("Tag %s removed" % identifier)

View File

@ -1,8 +1,10 @@
import falcon import falcon
import ipaddress import ipaddress
from datetime import datetime
from certidude import config from certidude import config
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.api.lease import parse_dn
def address_to_identity(cnx, addr): def address_to_identity(cnx, addr):
""" """
@ -10,19 +12,19 @@ def address_to_identity(cnx, addr):
""" """
SQL_LEASES = """ SQL_LEASES = """
SELECT select
acquired, acquired,
released, released,
identities.data as identity identities.data as identity
FROM from
addresses addresses
RIGHT JOIN right join
identities identities
ON on
identities.id = addresses.identity identities.id = addresses.identity
WHERE where
address = %s AND address = %s and
released IS NOT NULL released is not null
""" """
cursor = cnx.cursor() cursor = cnx.cursor()
@ -31,6 +33,7 @@ def address_to_identity(cnx, addr):
for acquired, released, identity in cursor: for acquired, released, identity in cursor:
return { return {
"address": addr,
"acquired": datetime.utcfromtimestamp(acquired), "acquired": datetime.utcfromtimestamp(acquired),
"identity": parse_dn(bytes(identity)) "identity": parse_dn(bytes(identity))
} }

View File

@ -29,7 +29,7 @@ def publish_certificate(func):
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()
push.publish("request_signed", csr.common_name) push.publish("request-signed", csr.common_name)
return cert return cert
return wrapped return wrapped
@ -93,7 +93,7 @@ def revoke_certificate(common_name):
cert = get_signed(common_name) cert = get_signed(common_name)
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
os.rename(cert.path, revoked_filename) os.rename(cert.path, revoked_filename)
push.publish("certificate_revoked", cert.fingerprint()) push.publish("certificate-revoked", cert.fingerprint())
def list_requests(directory=config.REQUESTS_DIR): def list_requests(directory=config.REQUESTS_DIR):
@ -141,7 +141,7 @@ def delete_request(common_name):
os.unlink(path) os.unlink(path)
# Publish event at CA channel # Publish event at CA channel
push.publish("request_deleted", request_sha1sum) push.publish("request-deleted", request_sha1sum)
# Write empty certificate to long-polling URL # Write empty certificate to long-polling URL
url = config.PUSH_PUBLISH % request_sha1sum url = config.PUSH_PUBLISH % request_sha1sum

View File

@ -13,7 +13,6 @@ import signal
import socket import socket
import subprocess import subprocess
import sys import sys
from certidude import authority
from certidude.signer import SignServer from certidude.signer import SignServer
from certidude.common import expand_paths from certidude.common import expand_paths
from datetime import datetime from datetime import datetime
@ -165,7 +164,7 @@ def certidude_setup_client(quiet, **kwargs):
@click.command("server", help="Set up OpenVPN server") @click.command("server", help="Set up OpenVPN server")
@click.argument("url") @click.argument("url")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default") @click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@ -252,7 +251,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") @click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
@expand_paths() @expand_paths()
def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote): def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate( retval = certidude_request_certificate(
url, url,
key_path, key_path,
@ -280,7 +279,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.command("server", help="Set up strongSwan server") @click.command("server", help="Set up strongSwan server")
@click.argument("url") @click.argument("url")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL) @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
@ -302,13 +301,17 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths() @expand_paths()
def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn): def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
if not local:
raise ValueError("Please specify local IP address")
if not os.path.exists(certificate_path): if not os.path.exists(certificate_path):
click.echo("As strongSwan server certificate needs specific key usage extensions please") click.echo("As strongSwan server certificate needs specific key usage extensions please")
click.echo("use following command to sign on Certidude server instead of web interface:") click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo() click.echo()
click.echo(" certidude sign %s" % common_name) click.echo(" certidude sign %s" % common_name)
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate( retval = certidude_request_certificate(
url, url,
key_path, key_path,
@ -368,7 +371,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths() @expand_paths()
def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction): def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate( retval = certidude_request_certificate(
url, url,
key_path, key_path,
@ -409,7 +412,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@expand_paths() @expand_paths()
def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote): def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate( retval = certidude_request_certificate(
url, url,
key_path, key_path,
@ -685,6 +688,8 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
# y - not valid yet # y - not valid yet
# r - revoked # r - revoked
from certidude import authority
from pycountry import countries from pycountry import countries
def dump_common(j): def dump_common(j):
@ -796,6 +801,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
@click.option("--lifetime", "-l", help="Lifetime") @click.option("--lifetime", "-l", help="Lifetime")
def certidude_sign(common_name, overwrite, lifetime): def certidude_sign(common_name, overwrite, lifetime):
from certidude import authority
request = authority.get_request(common_name) request = authority.get_request(common_name)
if request.signable: if request.signable:
# Sign via signer process # Sign via signer process

View File

@ -1,4 +1,7 @@
import os
import click
def expand_paths(): def expand_paths():
""" """
Prefix '..._path' keyword arguments of target function with 'directory' keyword argument Prefix '..._path' keyword arguments of target function with 'directory' keyword argument

View File

@ -2,11 +2,9 @@
import click import click
import os import os
import urllib.request import urllib.request
from certidude import config from certidude.wrappers import Certificate, Request
from OpenSSL import crypto from OpenSSL import crypto
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None): def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None):
""" """
Exchange CSR for certificate using Certidude HTTP API server Exchange CSR for certificate using Certidude HTTP API server

View File

@ -1,19 +1,19 @@
<section id="about">
<p>Hi {{session.username}},</p> <p>Hi {{session.username}},</p>
<p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p> <p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p>
<p>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p> <p>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
<p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %} <p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
<p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p> <p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p>
</section>
{% set s = session.certificate.identity %} {% set s = session.certificate.identity %}
<section id="requests">
<div id="requests">
<h1>Pending requests</h1> <h1>Pending requests</h1>
<ul id="pending_requests"> <ul id="pending_requests">
{% for request in session.requests %} {% for request in session.requests %}
{% include "request.html" %} {% include "request.html" %}
@ -23,19 +23,20 @@
<pre>certidude setup client {{session.common_name}}</pre> <pre>certidude setup client {{session.common_name}}</pre>
</li> </li>
</ul> </ul>
</div> </section>
<div id="signed"> <section id="signed">
<h1>Signed certificates</h1> <h1>Signed certificates</h1>
<input id="search" type="search" class="icon search">
<ul id="signed_certificates"> <ul id="signed_certificates">
{% for certificate in session.signed | sort | reverse %} {% for certificate in session.signed | sort | reverse %}
{% include "signed.html" %} {% include "signed.html" %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </section>
<div id="log"> <section id="log">
<h1>Log</h1> <h1>Log</h1>
<p> <p>
<input id="log_level_critical" type="checkbox" checked/> <label for="log_level_critical">Critical</label> <input id="log_level_critical" type="checkbox" checked/> <label for="log_level_critical">Critical</label>
@ -46,9 +47,9 @@
</p> </p>
<ul id="log_entries"> <ul id="log_entries">
</ul> </ul>
</div> </section>
<div id="revoked"> <section id="revoked">
<h1>Revoked certificates</h1> <h1>Revoked certificates</h1>
<p>To fetch certificate revocation list:</p> <p>To fetch certificate revocation list:</p>
<pre> <pre>
@ -72,4 +73,4 @@
<li>Great job! No certificate signing requests to sign.</li> <li>Great job! No certificate signing requests to sign.</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </section>

View File

@ -119,7 +119,7 @@ h2 svg {
top: 16px; top: 16px;
} }
p, td, footer, li, button, input { p, td, footer, li, button, input, select {
font-family: 'PT Sans Narrow'; font-family: 'PT Sans Narrow';
font-size: 14pt; font-size: 14pt;
} }
@ -159,6 +159,7 @@ pre {
display: inline; display: inline;
margin: 1mm 5mm 1mm 0; margin: 1mm 5mm 1mm 0;
line-height: 200%; line-height: 200%;
cursor: pointer;
} }
.icon{ .icon{
@ -170,24 +171,49 @@ pre {
text-decoration: none; text-decoration: none;
} }
li span.icon { #log_entries li span.icon {
background-size: 32px; background-size: 32px;
padding-left: 42px; padding-left: 42px;
padding-top: 2px; padding-top: 2px;
padding-bottom: 2px; padding-bottom: 2px;
} }
.tags .tag {
display: inline;
background-size: 32px;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 1em;
line-height: 200%;
cursor: pointer;
white-space: nowrap;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
border: none;
box-sizing: border-box;
}
.icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); }
.icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); } .icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
.icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); } .icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); } .icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); }
.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); } .icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); }
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); } .icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); }
.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); } .icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); }
.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); } .icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); }
.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); } .icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); }
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); }
.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); }
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); }
/* Make sure this is the last one */ /* Make sure this is the last one */
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="compass-7-icon" d="M256,90c91.74,0,166,74.243,166,166c0,91.741-74.245,166-166,166c-91.741,0-166-74.245-166-166
C90,164.259,164.244,90,256,90 M256,50C142.229,50,50,142.229,50,256s92.229,206,206,206s206-92.229,206-206S369.771,50,256,50z
M197.686,216.466l-28.355-47.135l47.225,28.408C209.145,202.733,202.736,209.099,197.686,216.466z M296.709,198.612
c6.459,4.562,12.119,10.179,16.729,16.602l29.232-45.883L296.709,198.612z M198.312,297.179l-28.982,45.492l45.416-28.936
C208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74
C309.596,303.253,303.299,309.593,296.018,314.604z M400.199,256.001l-99.238,21.998c-4.369,8.913-11.312,16.328-19.859,21.295
L256,400.2l-25.104-100.908c-8.545-4.965-15.488-12.381-19.857-21.293l-99.238-21.998l99.238-21.999
c4.369-8.913,11.312-16.328,19.857-21.294L256,111.8l25.104,100.908c8.545,4.966,15.488,12.381,19.857,21.294L400.199,256.001z
M278.406,256c0-12.374-10.031-22.407-22.406-22.407S233.592,243.626,233.592,256c0,12.376,10.033,22.408,22.408,22.408
S278.406,268.376,278.406,256z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="home-4-icon" d="M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492
z M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z"/>
</svg>

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="mobile-phone-6-icon" d="M139.59,131.775c-13.807,0-25,11.197-25,25.01V436.99c0,13.812,11.193,25.01,25,25.01h150.49
c13.807,0,25-11.198,25-25.01V156.766c0-13.802-11.186-24.99-24.98-24.99H139.59z M179.832,416.514h-30.996v-24.51h30.996V416.514z
M179.832,372.203h-30.996v-24.51h30.996V372.203z M230.334,416.514h-30.996v-24.51h30.996V416.514z M230.334,372.203h-30.996
v-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z
M280.836,312.887h-132V183.226h132V312.887z M283.451,111.408c13.445-0.01,26.9,5.113,37.164,15.369s15.4,23.699,15.41,37.147
h22.121c-0.012-19.113-7.312-38.231-21.898-52.805c-14.588-14.573-33.691-21.854-52.797-21.842V111.408z M283.451,72.682
c23.354-0.015,46.691,8.882,64.52,26.696c17.828,17.812,26.75,41.187,26.766,64.547h22.674c-0.02-29.166-11.16-58.358-33.418-80.597
C341.734,61.089,312.605,49.982,283.451,50V72.682z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="tag-2-icon" d="M234.508,50L50.068,50.262l-0.004,184.311L277.365,462l184.57-184.57L234.508,50z M114.877,167.365
c-15.027-15.027-15.027-39.395,0-54.424c15.029-15.029,39.396-15.029,54.426,0s15.029,39.396,0,54.424
C154.273,182.395,129.906,182.395,114.877,167.365z M242.316,327.94l-76.225-76.226l17.678-17.678l76.225,76.226L242.316,327.94z
M317.609,335.887L199.764,218.041l17.678-17.678l117.846,117.846L317.609,335.887z M351.818,301.678L233.973,183.832l17.678-17.678
L369.496,284L351.818,301.678z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -11,14 +11,14 @@
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"> <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
</head> </head>
<body> <body>
<div id="menu"> <nav id="menu">
<ul class="container"> <ul class="container">
<li>Requests</li> <li data-section="requests">Requests</li>
<li>Signed</li> <li data-section="signed">Signed</li>
<li>Revoked</li> <li data-section="revoked">Revoked</li>
<li>Log</li> <li data-section="log">Log</li>
</ul> </ul>
</div> </nav>
<div id="container" class="container"> <div id="container" class="container">
Loading certificate authority... Loading certificate authority...
</div> </div>

View File

@ -1,3 +1,159 @@
function onTagClicked() {
var value = $(this).html();
var updated = prompt("Enter new tag or clear to remove the tag", value);
if (updated == "") {
$(this).addClass("busy");
$.ajax({
method: "DELETE",
url: "/api/tag/" + $(this).attr("data-id")
});
} else if (updated && updated != value) {
$.ajax({
method: "PUT",
url: "/api/tag/" + $(this).attr("data-id"),
dataType: "json",
data: {
value: updated
}
});
}
}
function onNewTagClicked() {
var cn = $(event.target).attr("data-cn");
var key = $(event.target).val();
$(event.target).val("");
var value = prompt("Enter new " + key + " tag for " + cn);
if (!value) return;
if (value.length == 0) return;
$.ajax({
method: "POST",
url: "/api/tag/",
dataType: "json",
data: {
cn: cn,
value: value,
key: key
}
});
}
function onLogEntry (e) {
var entry = JSON.parse(e.data);
if ($("#log_level_" + entry.severity).prop("checked")) {
console.info("Received log entry:", entry);
$("#log_entries").prepend(nunjucks.render("logentry.html", {
entry: {
created: new Date(entry.created).toLocaleString(),
message: entry.message,
severity: entry.severity
}
}));
}
};
function onRequestSubmitted(e) {
console.log("Request submitted:", e.data);
$.ajax({
method: "GET",
url: "/api/request/" + e.data + "/",
dataType: "json",
success: function(request, status, xhr) {
console.info(request);
$("#pending_requests").prepend(
nunjucks.render('request.html', { request: request }));
}
});
}
function onRequestDeleted(e) {
console.log("Removing deleted request #" + e.data);
$("#request_" + e.data).remove();
}
function onClientUp(e) {
console.log("Adding security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: new Date(),
released: null
}}));
}
function onClientDown(e) {
console.log("Removing security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: null,
released: new Date()
}}));
}
function onRequestSigned(e) {
console.log("Request signed:", e.data);
$("#request_" + e.data).slideUp("normal", function() { $(this).remove(); });
$.ajax({
method: "GET",
url: "/api/signed/" + e.data + "/",
dataType: "json",
success: function(certificate, status, xhr) {
console.info(certificate);
$("#signed_certificates").prepend(
nunjucks.render('signed.html', { certificate: certificate }));
}
});
}
function onCertificateRevoked(e) {
console.log("Removing revoked certificate #" + e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
}
function onTagAdded(e) {
console.log("Tag added #" + e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
dataType: "json",
success: function(tag, status, xhr) {
// TODO: Deduplicate
$tag = $("<span id=\"tag_" + tag.id + "\" class=\"" + tag.key + " icon tag\" data-id=\""+tag.id+"\">" + tag.value + "</span>");
$tags = $("#signed_certificates [data-cn='" + tag.cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tag.cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
}
})
}
function onTagRemoved(e) {
console.log("Tag removed #" + e.data);
$("#tag_" + e.data).remove();
}
function onTagUpdated(e) {
console.log("Tag updated #" + e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
dataType: "json",
success:function(tag, status, xhr) {
console.info("Updated tag", tag);
$("#tag_" + tag.id).html(tag.value);
}
})
}
$(document).ready(function() { $(document).ready(function() {
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
$.ajax({ $.ajax({
@ -13,8 +169,6 @@ $(document).ready(function() {
$("#container").html(nunjucks.render('error.html', { message: msg })); $("#container").html(nunjucks.render('error.html', { message: msg }));
}, },
success: function(session, status, xhr) { success: function(session, status, xhr) {
console.info("Got:", session);
console.info("Opening EventSource from:", session.event_channel); console.info("Opening EventSource from:", session.event_channel);
var source = new EventSource(session.event_channel); var source = new EventSource(session.event_channel);
@ -23,90 +177,33 @@ $(document).ready(function() {
console.log("Received server-sent event:", event); console.log("Received server-sent event:", event);
} }
source.addEventListener("log-entry", function(e) { source.addEventListener("log-entry", onLogEntry);
var entry = JSON.parse(e.data); source.addEventListener("up-client", onClientUp);
console.info("Received log entry:", entry, "gonna prepend:", $("#log_level_" + entry.severity).prop("checked")); source.addEventListener("down-client", onClientDown);
if ($("#log_level_" + entry.severity).prop("checked")) { source.addEventListener("request-deleted", onRequestDeleted);
$("#log_entries").prepend(nunjucks.render("logentry.html", { source.addEventListener("request-submitted", onRequestSubmitted);
entry: { source.addEventListener("request-signed", onRequestSigned);
created: new Date(entry.created).toLocaleString(), source.addEventListener("certificate-revoked", onCertificateRevoked);
message: entry.message, source.addEventListener("tag-added", onTagAdded);
severity: entry.severity source.addEventListener("tag-removed", onTagRemoved);
} source.addEventListener("tag-updated", onTagUpdated);
}));
}
});
source.addEventListener("up-client", function(e) {
console.log("Adding security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: new Date(),
released: null
}}));
});
source.addEventListener("down-client", function(e) {
console.log("Removing security association:" + e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('status.html', {
lease: {
address: lease.address,
identity: lease.identity,
acquired: null,
released: new Date()
}}));
});
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);
$.ajax({
method: "GET",
url: "/api/request/" + e.data + "/",
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(); });
$.ajax({
method: "GET",
url: "/api/signed/" + e.data + "/",
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) {
console.log("Removing revoked certificate #" + e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
});
/**
* Render authority views
**/
$("#container").html(nunjucks.render('authority.html', { session: session, window: window })); $("#container").html(nunjucks.render('authority.html', { session: session, window: window }));
console.info("Swtiching to requests section");
$("section").hide();
$("section#requests").show();
$("nav#menu li").click(function(e) {
$("section").hide();
$("section#" + $(e.target).attr("data-section")).show();
});
/**
* Fetch log entries
*/
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "/api/log/", url: "/api/log/",
@ -127,6 +224,52 @@ $(document).ready(function() {
} }
}); });
/**
* Set up search bar
*/
$(window).on("search", function() {
var q = $("#search").val();
$(".filterable").each(function(i, e) {
if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) {
$(e).show();
} else {
$(e).hide();
}
});
});
/**
* Bind key up event of search bar
*/
$("#search").on("keyup", function() {
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
console.info("Setting timeout", window.searchTimeout);
});
/**
* Fetch tags for certificates
*/
$.ajax({
method: "GET",
url: "/api/tag/",
dataType: "json",
success:function(tags, status, xhr) {
console.info("Got", tags.length, "tags");
for (var j = 0; j < tags.length; j++) {
// TODO: Deduplicate
$tag = $("<span id=\"tag_" + tags[j].id + "\" class=\"" + tags[j].key + " icon tag\" data-id=\""+tags[j].id+"\">" + tags[j].value + "</span>");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
}
}
});
/**
* Fetch leases associated with certificates
*/
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "/api/lease/", url: "/api/lease/",
@ -148,17 +291,6 @@ $(document).ready(function() {
}})); }}));
} }
/* 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();
}
});
});
} }
}); });
} }

View File

@ -1,4 +1,4 @@
<li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" class="filterable"> <li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable">
<a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a> <a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a>
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button> <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
@ -25,6 +25,15 @@
{{certificate.key_usage}} {{certificate.key_usage}}
</div> </div>
<div class="tags">
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();">
<option value="">Add tag...</option>
<option value="location">Location</option>
<option value="phone">Phone</option>
<option value="room">Room</option>
</select>
</div>
<div class="status"> <div class="status">
{% include 'status.html' %} {% include 'status.html' %}
</div> </div>

View File

@ -18,11 +18,11 @@ conn site-to-clients
rightsourceip={{subnet}} # Serve virtual IP-s from this pool rightsourceip={{subnet}} # Serve virtual IP-s from this pool
left={{local}} # Gateway IP address left={{local}} # Gateway IP address
leftcert={{certificate_path}} # Gateway certificate leftcert={{certificate_path}} # Gateway certificate
{% if route %} {% if route %}
{% if route | length == 1 %} {% if route | length == 1 %}
leftsubnet={{route[0]}} # Advertise routes via this connection leftsubnet={{route[0]}} # Advertise routes via this connection
{% else %} {% else %}
leftsubnet={ {{ route | join(', ') }} } leftsubnet={ {{ route | join(', ') }} }
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -3,7 +3,6 @@ import hashlib
import re import re
import click import click
import io import io
from certidude import push
from Crypto.Util import asn1 from Crypto.Util import asn1
from OpenSSL import crypto from OpenSSL import crypto
from datetime import datetime from datetime import datetime