api: Preliminary API-fication of user interface

This commit is contained in:
Lauri Võsandi 2015-11-11 20:12:04 +01:00
parent a413a15854
commit 4eb0cceacc
17 changed files with 406 additions and 46 deletions

View File

@ -7,8 +7,10 @@ import json
import types import types
import click import click
from time import sleep from time import sleep
from certidude.wrappers import Request, Certificate, CertificateAuthorityConfig from certidude.wrappers import Request, Certificate, CertificateAuthority, \
CertificateAuthorityConfig
from certidude.auth import login_required from certidude.auth import login_required
from OpenSSL import crypto
from pyasn1.codec.der import decoder from pyasn1.codec.der import decoder
from datetime import datetime, date from datetime import datetime, date
from jinja2 import Environment, PackageLoader, Template from jinja2 import Environment, PackageLoader, Template
@ -26,6 +28,7 @@ def event_source(func):
if req.get_header("Accept") == "text/event-stream": if req.get_header("Accept") == "text/event-stream":
resp.status = falcon.HTTP_SEE_OTHER resp.status = falcon.HTTP_SEE_OTHER
resp.location = ca.push_server + "/ev/" + ca.uuid resp.location = ca.push_server + "/ev/" + ca.uuid
resp.body = "Redirecting to:" + resp.location
print("Delegating EventSource handling to:", resp.location) print("Delegating EventSource handling to:", resp.location)
return func(self, req, resp, ca, *args, **kwargs) return func(self, req, resp, ca, *args, **kwargs)
return wrapped return wrapped
@ -72,7 +75,24 @@ def validate_common_name(func):
class MyEncoder(json.JSONEncoder): class MyEncoder(json.JSONEncoder):
REQUEST_ATTRIBUTES = "signable", "subject", "changed", "common_name", \
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage"
CERTIFICATE_ATTRIBUTES = "revokable", "subject", "changed", "common_name", \
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"key_type", "key_length", "sha256sum", "serial_number", "key_usage"
def default(self, obj): def default(self, obj):
if isinstance(obj, crypto.X509Name):
try:
return "".join(["/%s=%s" % (k.decode("ascii"),v.decode("utf-8")) for k, v in obj.get_components()])
except UnicodeDecodeError: # Work around old buggy pyopenssl
return "".join(["/%s=%s" % (k.decode("ascii"),v.decode("iso8859")) for k, v in obj.get_components()])
if isinstance(obj, ipaddress._IPAddressBase):
return str(obj)
if isinstance(obj, set):
return tuple(obj)
if isinstance(obj, datetime): if isinstance(obj, datetime):
return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
if isinstance(obj, date): if isinstance(obj, date):
@ -81,6 +101,26 @@ class MyEncoder(json.JSONEncoder):
return tuple(obj) return tuple(obj)
if isinstance(obj, types.GeneratorType): if isinstance(obj, types.GeneratorType):
return tuple(obj) return tuple(obj)
if isinstance(obj, Request):
return dict([(key, getattr(obj, key)) for key in self.REQUEST_ATTRIBUTES \
if hasattr(obj, key) and getattr(obj, key)])
if isinstance(obj, Certificate):
return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \
if hasattr(obj, key) and getattr(obj, key)])
if isinstance(obj, CertificateAuthority):
return dict(
slug = obj.slug,
certificate = obj.certificate,
admin_users = obj.admin_users,
autosign_subnets = obj.autosign_subnets,
request_subnets = obj.request_subnets,
admin_subnets=obj.admin_subnets,
requests=obj.get_requests(),
signed=obj.get_signed(),
revoked=obj.get_revoked()
)
if hasattr(obj, "serialize"):
return obj.serialize()
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
@ -94,7 +134,7 @@ def serialize(func):
resp.set_header("Pragma", "no-cache"); resp.set_header("Pragma", "no-cache");
resp.set_header("Expires", "0"); resp.set_header("Expires", "0");
r = func(instance, req, resp, **kwargs) r = func(instance, req, resp, **kwargs)
if not resp.body: if resp.body is None:
if not req.client_accepts_json: if not req.client_accepts_json:
raise falcon.HTTPUnsupportedMediaType( raise falcon.HTTPUnsupportedMediaType(
"This API only supports the JSON media type.", "This API only supports the JSON media type.",
@ -118,6 +158,9 @@ def templatize(path):
resp.set_header("Pragma", "no-cache"); resp.set_header("Pragma", "no-cache");
resp.set_header("Expires", "0"); resp.set_header("Expires", "0");
resp.set_header("Content-Type", "application/json") resp.set_header("Content-Type", "application/json")
r.pop("req")
r.pop("resp")
r.pop("user")
resp.body = json.dumps(r, cls=MyEncoder) resp.body = json.dumps(r, cls=MyEncoder)
return r return r
else: else:
@ -174,7 +217,7 @@ class SignedCertificateListResource(CertificateAuthorityBase):
l=j.city, l=j.city,
o=j.organization, o=j.organization,
ou=j.organizational_unit, ou=j.organizational_unit,
fingerprint=j.fingerprint) fingerprint=j.fingerprint())
class RequestDetailResource(CertificateAuthorityBase): class RequestDetailResource(CertificateAuthorityBase):
@ -330,13 +373,22 @@ class CertificateAuthorityResource(CertificateAuthorityBase):
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug) resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug)
class IndexResource(CertificateAuthorityBase): class IndexResource(CertificateAuthorityBase):
@serialize
@login_required @login_required
@pop_certificate_authority @pop_certificate_authority
@authorize_admin @authorize_admin
@event_source @event_source
@templatize("index.html")
def on_get(self, req, resp, ca, user): def on_get(self, req, resp, ca, user):
return locals() return ca
class AuthorityListResource(CertificateAuthorityBase):
@serialize
@login_required
def on_get(self, req, resp, user):
return dict(
authorities=(self.config.ca_list), # TODO: Check if user is CA admin
username=user[0]
)
class ApplicationConfigurationResource(CertificateAuthorityBase): class ApplicationConfigurationResource(CertificateAuthorityBase):
@pop_certificate_authority @pop_certificate_authority
@ -377,14 +429,16 @@ class StaticResource(object):
if not path.startswith(self.root): if not path.startswith(self.root):
raise falcon.HTTPForbidden raise falcon.HTTPForbidden
if os.path.isdir(path):
path = os.path.join(path, "index.html")
print("Serving:", path) print("Serving:", path)
if os.path.exists(path): if os.path.exists(path):
content_type, content_encoding = mimetypes.guess_type(path) content_type, content_encoding = mimetypes.guess_type(path)
if content_type: if content_type:
resp.append_header("Content-Type", content_type) resp.append_header("Content-Type", content_type)
if content_encoding: if content_encoding:
resp.append_header("Content-Encoding", content_encoding) resp.append_header("Content-Encoding", content_encoding)
resp.append_header("Content-Disposition", "attachment")
resp.stream = open(path, "rb") resp.stream = open(path, "rb")
else: else:
resp.status = falcon.HTTP_404 resp.status = falcon.HTTP_404
@ -396,14 +450,14 @@ def certidude_app():
config = CertificateAuthorityConfig() config = CertificateAuthorityConfig()
app = falcon.API() app = falcon.API()
app.add_route("/api/{ca}/ocsp/", CertificateStatusResource(config)) app.add_route("/api/ca/{ca}/ocsp/", CertificateStatusResource(config))
app.add_route("/api/{ca}/signed/{cn}/openvpn", ApplicationConfigurationResource(config)) app.add_route("/api/ca/{ca}/signed/{cn}/openvpn", ApplicationConfigurationResource(config))
app.add_route("/api/{ca}/certificate/", CertificateAuthorityResource(config)) app.add_route("/api/ca/{ca}/certificate/", CertificateAuthorityResource(config))
app.add_route("/api/{ca}/revoked/", RevocationListResource(config)) app.add_route("/api/ca/{ca}/revoked/", RevocationListResource(config))
app.add_route("/api/{ca}/signed/{cn}/", SignedCertificateDetailResource(config)) app.add_route("/api/ca/{ca}/signed/{cn}/", SignedCertificateDetailResource(config))
app.add_route("/api/{ca}/signed/", SignedCertificateListResource(config)) app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config))
app.add_route("/api/{ca}/request/{cn}/", RequestDetailResource(config)) app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config))
app.add_route("/api/{ca}/request/", RequestListResource(config)) app.add_route("/api/ca/{ca}/request/", RequestListResource(config))
app.add_route("/api/{ca}/", IndexResource(config)) app.add_route("/api/ca/{ca}/", IndexResource(config))
app.add_route("/api/ca/", AuthorityListResource(config))
return app return app

View File

@ -0,0 +1,82 @@
<h1>{{authority.slug}} management</h1>
<p>Hi {{session.username}},</p>
<p>Request submission is allowed from: {% if authority.request_subnets %}{% for i in authority.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p>
<p>Autosign is allowed from: {% if authority.autosign_subnets %}{% for i in authority.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
<p>Authority administration is allowed from: {% if authority.admin_subnets %}{% for i in authority.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
<p>Authority administration allowed for: {% for i in authority.admin_users %}{{ i }} {% endfor %}</p>
{% set s = authority.certificate.subject %}
<h1>Pending requests</h1>
<ul>
{% for j in authority.requests %}
{% include "request.html" %}
{% else %}
<li>Great job! No certificate signing requests to sign.</li>
{% endfor %}
</ul>
<h1>Signed certificates</h1>
<ul>
{% for j in authority.signed | sort | reverse %}
<li id="certificate_{{ j.sha256sum }}">
<a class="button" href="/api/{{authority.slug}}/signed/{{j.subject}}/">Fetch</a>
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
<div class="monospace">
{% include 'iconmonstr-certificate-15-icon.svg' %}
{{j.subject}}
</div>
{% if j.email_address %}
<div class="email">{% include 'iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div>
{% endif %}
<div class="monospace">
{% include 'iconmonstr-key-2-icon.svg' %}
<span title="SHA-256 of public key">
{{ j.sha256sum }}
</span>
{{ j.key_length }}-bit
{{ j.key_type }}
</div>
<div>
{% include 'iconmonstr-flag-3-icon.svg' %}
{{j.key_usage}}
</div>
</li>
{% endfor %}
</ul>
<h1>Revoked certificates</h1>
<p>To fetch certificate revocation list:</p>
<pre>
curl {{request.url}}/revoked/ | openssl crl -text -noout
</pre>
<!--
<p>To perform online certificate status request</p>
<pre>
curl {{request.url}}/certificate/ > authority.pem
openssl ocsp -issuer authority.pem -CAfile authority.pem -url {{request.url}}/ocsp/ -serial 0x
</pre>
-->
<ul>
{% for j in authority.revoked %}
<li id="certificate_{{ j.sha256sum }}">
{{j.changed}}
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
</li>
{% else %}
<li>Great job! No certificate signing requests to sign.</li>
{% endfor %}
</ul>

View File

@ -1,3 +1,24 @@
@font-face {
font-family: 'PT Sans Narrow';
font-style: normal;
font-weight: 400;
src: local('PT Sans Narrow'), local('PTSans-Narrow'), url('../fonts/pt-sans.woff2') format('woff2');
}
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Mono'), local('UbuntuMono-Regular'), url('../fonts/ubuntu-mono.woff2') format('woff2');
}
@font-face {
font-family: 'Gentium Basic';
font-style: normal;
font-weight: 400;
src: local('Gentium Basic'), local('GentiumBasic'), url('../fonts/gentium-basic.woff2') format('woff2');
}
svg { svg {
position: relative; position: relative;
top: 0.5em; top: 0.5em;
@ -55,7 +76,7 @@ html,body {
body { body {
background: #222; background: #222;
background-image: url('//fc00.deviantart.net/fs71/i/2013/078/9/6/free_hexa_pattern_cc0_by_black_light_studio-d4ig12f.png'); background-image: url('../img/free_hexa_pattern_cc0_by_black_light_studio.png');
background-position: center; background-position: center;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License Agreement at http://iconmonstr.com/license/ -->
<!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" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path id="certificate-15" d="M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727
c-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294
c4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672
c-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639
c4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z
M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572
c-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677
c-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564
c0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649
c12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564
c4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292
c-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95
c-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z
M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797
C346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799
c-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352
c29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119
C294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6
c0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259
c-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028
c-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617
l94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49
c9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67
L243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245
c-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5
L227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905
l10.713,13.597l86.679-51.048L281.516,237.182z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,21 @@
<?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="email-2-icon" d="M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465
L96.473,138.407H415.533z M84.744,173.506l85.504,65.441L84.744,324.45V173.506z M85.1,373.593l113.186-113.186l57.654,44.127
l57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z"/>
</svg>

After

Width:  |  Height:  |  Size: 982 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License Agreement at http://iconmonstr.com/license/ -->
<!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="flag-3-icon" d="M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642
c-60.271,0-61.627-51.923-131.596-51.923c-37.832,0-73.106,17.577-88.045,30.381c0,12.64,0,216.762,0,216.762
c21.204-14.696,53.426-30.144,88.286-30.144c66.08,0,75.343,49.388,134.242,49.388c38.042,0,64.437-24.369,64.437-24.369V80.746z"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@ -0,0 +1,15 @@
<?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="key-2-icon" stroke="#000000" stroke-miterlimit="10" d="M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872
l-45.746-0.001v41.345l-100.004-0.001l150.078-150.076c-4.578-4.686-10.061-11.391-13.691-17.423L50,426.498v-40.939
l145.736-145.736C212.174,278.996,244.713,310.705,286.529,325.486z M425.646,92.339c48.473,48.473,48.471,127.064-0.002,175.535
c-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537
C298.58,43.865,377.172,43.865,425.646,92.339z M400.73,117.165c-12.023-12.021-31.516-12.021-43.537,0
c-12.021,12.022-12.021,31.517,0,43.538s31.514,12.021,43.537-0.001C412.754,148.68,412.75,129.188,400.73,117.165z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,18 @@
<?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="time-13-icon" d="M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933
c0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13
c-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13
c-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096
c-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202
c-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13
c7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z
M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5
H197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Certidude server</title>
<link href="/css/style.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="/js/nunjucks.min.js"></script>
<script type="text/javascript" src="/js/certidude.js"></script>
</head>
<body>
<div id="container">
Loading certificate authority...
</div>
</body>
<footer>
<a href="http://github.com/laurivosandi/certidude">Certidude</a> by
<a href="http://github.com/laurivosandi/">Lauri Võsandi</a>
</footer>
</html>

View File

@ -1,30 +1,56 @@
$(document).ready(function() { $(document).ready(function() {
console.info("Opening EventSource from:", window.location.href); console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
var source = new EventSource(window.location.href); $.ajax({
method: "GET",
url: "/api/ca/",
dataType: "json",
success: function(session, status, xhr) {
console.info("Loaded CA list:", session);
source.onmessage = function(event) { if (!session.authorities) {
console.log("Received server-sent event:", event); alert("No certificate authorities to manage! Have you created one yet?");
} return;
}
source.addEventListener("request_deleted", function(e) { $.ajax({
console.log("Removing deleted request #" + e.data); method: "GET",
$("#request_" + e.data).remove(); url: "/api/ca/" + session.authorities[0],
dataType: "json",
success: function(authority, status, xhr) {
console.info("Got CA:", authority);
console.info("Opening EventSource from:", "/api/ca/" + authority.slug);
var source = new EventSource("/api/" + authority.slug);
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();
});
$("#container").html(nunjucks.render('authority.html', { authority: authority, session: session }));
}
});
}
}); });
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();
});
}); });

4
certidude/static/js/nunjucks.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
<li id="request_{{ j.md5sum }}">
<a class="button" href="/api/{{authority.slug}}/request/{{j.common_name}}/">Fetch</a>
{% if j.signable %}
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button>
{% else %}
<button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button>
{% endif %}
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/request/{{j.common_name}}/',type:'delete'});">Delete</button>
<div class="monospace">
{% include 'iconmonstr-certificate-15-icon.svg' %}
{{j.subject}}
</div>
{% if j.email_address %}
<div class="email">{% include 'iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div>
{% endif %}
<div class="monospace">
{% include 'iconmonstr-key-2-icon.svg' %}
<span title="SHA-1 of public key">
{{ j.sha256sum }}
</span>
{{ j.key_length }}-bit
{{ j.key_type }}
</div>
{% set key_usage = j.key_usage %}
{% if key_usage %}
<div>
{% include 'iconmonstr-flag-3-icon.svg' %}
{{j.key_usage}}
</div>
{% endif %}
</li>

View File

@ -299,11 +299,21 @@ class CertificateBase:
assert len(h) * 4 == self.key_length, "%s is not %s" % (len(h)*4, self.key_length) assert len(h) * 4 == self.key_length, "%s is not %s" % (len(h)*4, self.key_length)
return re.findall("\d\d", h) return re.findall("\d\d", h)
def fingerprint(self): def fingerprint(self, algorithm="sha256"):
import binascii return hashlib.new(algorithm, self.buf.encode("ascii")).hexdigest()
m, _ = self.pubkey
return "%x" % m @property
return ":".join(re.findall("..", hashlib.sha1(binascii.unhexlify("%x" % m)).hexdigest())) def md5sum(self):
return self.fingerprint("md5")
@property
def sha1sum(self):
return self.fingerprint("sha1")
@property
def sha256sum(self):
return self.fingerprint("sha256")
class Request(CertificateBase): class Request(CertificateBase):
def __init__(self, mixed=None): def __init__(self, mixed=None):