1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Several changes

* OCSP workaround for StrongSwan
* Machine attributes framework
* Scripting support
* Default to nginx frontend
This commit is contained in:
Lauri Võsandi 2017-07-05 18:22:03 +03:00
parent d08a3f9f92
commit 9b5511212e
20 changed files with 211 additions and 93 deletions

View File

@ -8,6 +8,7 @@ import click
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from time import sleep from time import sleep
from xattr import listxattr, getxattr
from certidude import authority, mailer from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.user import User from certidude.user import User
@ -32,7 +33,6 @@ class SessionResource(object):
@serialize @serialize
@login_required @login_required
def on_get(self, req, resp): def on_get(self, req, resp):
import xattr
def serialize_requests(g): def serialize_requests(g):
for common_name, path, buf, obj, server in g(): for common_name, path, buf, obj, server in g():
@ -50,7 +50,7 @@ class SessionResource(object):
# Extract certificate tags from filesystem # Extract certificate tags from filesystem
try: try:
tags = [] tags = []
for tag in xattr.getxattr(path, "user.xdg.tags").split(","): for tag in getxattr(path, "user.xdg.tags").split(","):
if "=" in tag: if "=" in tag:
k, v = tag.split("=", 1) k, v = tag.split("=", 1)
else: else:
@ -59,12 +59,17 @@ class SessionResource(object):
except IOError: # No such attribute(s) except IOError: # No such attribute(s)
tags = None tags = None
attributes = {}
for key in listxattr(path):
if key.startswith("user.machine."):
attributes[key[13:]] = getxattr(path, key)
# Extract lease information from filesystem # Extract lease information from filesystem
try: try:
last_seen = datetime.strptime(xattr.getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ")
lease = dict( lease = dict(
inner_address = xattr.getxattr(path, "user.lease.inner_address"), inner_address = getxattr(path, "user.lease.inner_address"),
outer_address = xattr.getxattr(path, "user.lease.outer_address"), outer_address = getxattr(path, "user.lease.outer_address"),
last_seen = last_seen, last_seen = last_seen,
age = datetime.utcnow() - last_seen age = datetime.utcnow() - last_seen
) )
@ -80,7 +85,8 @@ class SessionResource(object):
expires = obj.not_valid_after, expires = obj.not_valid_after,
sha256sum = hashlib.sha256(buf).hexdigest(), sha256sum = hashlib.sha256(buf).hexdigest(),
lease = lease, lease = lease,
tags = tags tags = tags,
attributes = attributes or None,
) )
if req.context.get("user").is_admin(): if req.context.get("user").is_admin():
@ -160,7 +166,7 @@ import ipaddress
class NormalizeMiddleware(object): class NormalizeMiddleware(object):
def process_request(self, req, resp, *args): def process_request(self, req, resp, *args):
assert not req.get_param("unicode") or req.get_param("unicode") == u"", "Unicode sanity check failed" assert not req.get_param("unicode") or req.get_param("unicode") == u"", "Unicode sanity check failed"
req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0].decode("utf-8"))
def process_response(self, req, resp, resource=None): def process_response(self, req, resp, resource=None):
# wtf falcon?! # wtf falcon?!
@ -181,6 +187,7 @@ def certidude_app(log_handlers=[]):
app = falcon.API(middleware=NormalizeMiddleware()) app = falcon.API(middleware=NormalizeMiddleware())
app.req_options.auto_parse_form_urlencoded = True app.req_options.auto_parse_form_urlencoded = True
#app.req_options.strip_url_path_trailing_slash = False
# Certificate authority API calls # Certificate authority API calls
app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/certificate/", CertificateAuthorityResource())
@ -194,7 +201,7 @@ def certidude_app(log_handlers=[]):
app.add_route("/api/token/", TokenResource()) app.add_route("/api/token/", TokenResource())
# Extended attributes for scripting etc. # Extended attributes for scripting etc.
app.add_route("/api/signed/{cn}/attr/", AttributeResource()) app.add_route("/api/signed/{cn}/attr/", AttributeResource(namespace="machine"))
app.add_route("/api/signed/{cn}/script/", ScriptResource()) app.add_route("/api/signed/{cn}/script/", ScriptResource())
# API calls used by pushed events on the JS end # API calls used by pushed events on the JS end
@ -215,18 +222,13 @@ def certidude_app(log_handlers=[]):
from .scep import SCEPResource from .scep import SCEPResource
app.add_route("/api/scep/", SCEPResource()) app.add_route("/api/scep/", SCEPResource())
if config.OCSP_SUBNETS:
from .ocsp import OCSPResource
app.add_route("/api/ocsp/", OCSPResource())
# Add sink for serving static files # Add sink for serving static files
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
def log_exceptions(ex, req, resp, params): if config.OCSP_SUBNETS:
logger.debug("Caught exception: %s" % ex) from .ocsp import OCSPResource
raise ex app.add_sink(OCSPResource(), prefix="/api/ocsp")
app.add_error_handler(Exception, log_exceptions)
# Set up log handlers # Set up log handlers
if config.LOGGING_BACKEND == "sql": if config.LOGGING_BACKEND == "sql":

View File

@ -1,14 +1,24 @@
import click
import falcon import falcon
import logging import logging
from ipaddress import ip_address import re
from xattr import setxattr, listxattr, removexattr
from datetime import datetime from datetime import datetime
from certidude import config, authority from certidude import config, authority, push
from certidude.decorators import serialize from certidude.decorators import serialize, csrf_protection
from certidude.firewall import whitelist_subject
from certidude.auth import login_required, login_optional, authorize_admin
from ipaddress import ip_address
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AttributeResource(object): class AttributeResource(object):
def __init__(self, namespace):
self.namespace = namespace
@serialize @serialize
@login_required
@authorize_admin
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
""" """
Return extended attributes stored on the server. Return extended attributes stored on the server.
@ -17,22 +27,32 @@ class AttributeResource(object):
Results made available only to lease IP address. Results made available only to lease IP address.
""" """
try: try:
path, buf, cert, attribs = authority.get_attributes(cn) path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace)
except IOError: except IOError:
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
else: else:
try:
whitelist = ip_address(attribs.get("user").get("lease").get("inner_address").decode("ascii"))
except AttributeError: # TODO: probably race condition
raise falcon.HTTPForbidden("Forbidden",
"Attributes only accessible to the machine")
if req.context.get("remote_addr") != whitelist:
logger.info("Attribute access denied from %s, expected %s for %s",
req.context.get("remote_addr"),
whitelist,
cn)
raise falcon.HTTPForbidden("Forbidden",
"Attributes only accessible to the machine")
return attribs return attribs
@csrf_protection
@whitelist_subject # TODO: sign instead
def on_post(self, req, resp, cn):
try:
path, buf, cert = authority.get_signed(cn)
except IOError:
raise falcon.HTTPNotFound()
else:
for key in req.params:
if not re.match("[a-z0-9_\.]+$", key):
raise falcon.HTTPBadRequest("Invalid key")
valid = set()
for key, value in req.params.items():
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
setxattr(path, identifier, value.encode("utf-8"))
valid.add(identifier)
for key in listxattr(path):
if not key.startswith("user.%s." % self.namespace):
continue
if key not in valid:
removexattr(path, key)
push.publish("attribute-update", cn)

View File

@ -1,4 +1,3 @@
from __future__ import unicode_literals, division, absolute_import, print_function
import click import click
import hashlib import hashlib
import os import os
@ -14,26 +13,35 @@ from oscrypto import keys, asymmetric, symmetric
from oscrypto.errors import SignatureError from oscrypto.errors import SignatureError
class OCSPResource(object): class OCSPResource(object):
def on_post(self, req, resp): def __call__(self, req, resp):
if req.method == "GET":
_, _, _, tail = req.path.split("/", 3)
body = b64decode(tail)
elif req.method == "POST":
body = req.stream.read(req.content_length or 0)
else:
raise falcon.HTTPMethodNotAllowed()
fh = open(config.AUTHORITY_CERTIFICATE_PATH) fh = open(config.AUTHORITY_CERTIFICATE_PATH)
server_certificate = asymmetric.load_certificate(fh.read()) server_certificate = asymmetric.load_certificate(fh.read())
fh.close() fh.close()
ocsp_req = ocsp.OCSPRequest.load(req.stream.read()) ocsp_req = ocsp.OCSPRequest.load(body)
print(ocsp_req["tbs_request"].native)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
response_extensions = [] response_extensions = []
for ext in ocsp_req["tbs_request"]["request_extensions"]: try:
if ext["extn_id"] == "nonce": for ext in ocsp_req["tbs_request"]["request_extensions"]:
response_extensions.append( if ext["extn_id"].native == "nonce":
ocsp.ResponseDataExtension({ response_extensions.append(
'extn_id': "nonce", ocsp.ResponseDataExtension({
'critical': False, 'extn_id': "nonce",
'extn_value': ext["extn_value"] 'critical': False,
}) 'extn_value': ext["extn_value"]
) })
)
except ValueError: # https://github.com/wbond/asn1crypto/issues/56
pass
responses = [] responses = []
for item in ocsp_req["tbs_request"]["request_list"]: for item in ocsp_req["tbs_request"]["request_list"]:

View File

@ -1,4 +1,3 @@
from __future__ import unicode_literals, division, absolute_import, print_function
import click import click
import hashlib import hashlib
import os import os

View File

@ -1,21 +1,37 @@
import falcon import falcon
import logging import logging
from certidude import config, authority from certidude import const, config, authority
from certidude.decorators import serialize from certidude.decorators import serialize
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from certidude.firewall import whitelist_subject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True)
class ScriptResource(): class ScriptResource():
@whitelist_subject
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
try: try:
path, buf, cert, attribs = authority.get_attributes(cn) path, buf, cert, attribs = authority.get_attributes(cn)
except IOError: except IOError:
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
else: else:
resp.set_header("Content-Type", "text/x-shellscript") script = config.SCRIPT_DEFAULT
resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs) tags = []
for tag in attribs.get("user").get("xdg").get("tags").split(","):
if "=" in tag:
k, v = tag.split("=", 1)
else:
k, v = "other", tag
if k == "script":
script = v
tags.append(dict(id=tag, key=k, value=v))
resp.set_header("Content-Type", "text/x-shellscript")
resp.body = env.get_template(script).render(
authority_name=const.FQDN,
common_name=cn,
tags=tags,
attributes=attribs.get("user").get("machine"))
logger.info("Served script %s for %s at %s" % (script, cn, req.context["remote_addr"]))
# TODO: Assert time is within reasonable range # TODO: Assert time is within reasonable range

View File

@ -1,4 +1,4 @@
from __future__ import unicode_literals, division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import click import click
import os import os
import random import random
@ -57,17 +57,19 @@ def get_revoked(serial):
datetime.utcfromtimestamp(os.stat(path).st_ctime) datetime.utcfromtimestamp(os.stat(path).st_ctime)
def get_attributes(cn): def get_attributes(cn, namespace=None):
path, buf, cert = get_signed(cn) path, buf, cert = get_signed(cn)
attribs = dict() attribs = dict()
for key in listxattr(path): for key in listxattr(path):
if not key.startswith("user."): if not key.startswith("user."):
continue continue
if namespace and not key.startswith("user.%s." % namespace):
continue
value = getxattr(path, key) value = getxattr(path, key)
current = attribs current = attribs
if "." in key: if "." in key:
namespace, key = key.rsplit(".", 1) prefix, key = key.rsplit(".", 1)
for component in namespace.split("."): for component in prefix.split("."):
if component not in current: if component not in current:
current[component] = dict() current[component] = dict()
current = current[component] current = current[component]
@ -324,7 +326,6 @@ def sign(common_name, overwrite=False):
def _sign(csr, buf, overwrite=False): def _sign(csr, buf, overwrite=False):
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
assert isinstance(csr, x509.CertificateSigningRequest) assert isinstance(csr, x509.CertificateSigningRequest)
from xattr import getxattr, listxattr, setxattr
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)

View File

@ -1004,24 +1004,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
certidude_path = sys.argv[0] certidude_path = sys.argv[0]
# Push server config generation click.echo("Generating: %s" % nginx_config.name)
if os.path.exists("/etc/nginx"): nginx_config.write(env.get_template("server/nginx.conf").render(vars()))
listen = "127.0.1.1" nginx_config.close()
port = "8080" if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
click.echo("Generating: %s" % nginx_config.name) os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
nginx_config.write(env.get_template("server/nginx.conf").render(vars())) click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
nginx_config.close() if os.path.exists("/etc/nginx/sites-enabled/default"):
if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): os.unlink("/etc/nginx/sites-enabled/default")
os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") os.system("service nginx restart")
click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
if os.path.exists("/etc/nginx/sites-enabled/default"):
os.unlink("/etc/nginx/sites-enabled/default")
os.system("service nginx restart")
else:
click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration")
click.echo("Remember to install/configure nchan capable nginx instead of regular nginx!")
listen = "0.0.0.0"
port = "80"
if os.path.exists("/etc/systemd"): if os.path.exists("/etc/systemd"):
if os.path.exists("/etc/systemd/system/certidude.service"): if os.path.exists("/etc/systemd/system/certidude.service"):
@ -1284,16 +1275,19 @@ def certidude_cron():
@click.command("serve", help="Run server") @click.command("serve", help="Run server")
@click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler") @click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler")
@click.option("-p", "--port", default=80, help="Listen port") @click.option("-p", "--port", default=8080, help="Listen port")
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address") @click.option("-l", "--listen", default="127.0.0.1", help="Listen address")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_serve(port, listen, fork, exit_handler): def certidude_serve(port, listen, fork, exit_handler):
import pwd import pwd
from setproctitle import setproctitle from setproctitle import setproctitle
from certidude.signer import SignServer from certidude.signer import SignServer
from certidude import authority, const from certidude import authority, const
click.echo("Using configuration from: %s" % const.CONFIG_PATH)
if port == 80:
click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")
click.echo("Using configuration from: %s" % const.CONFIG_PATH)
log_handlers = [] log_handlers = []

View File

@ -98,4 +98,4 @@ TOKEN_SECRET = cp.get("token", "secret")
# The API call for looking up scripts uses following directory as root # The API call for looking up scripts uses following directory as root
SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script")
SCRIPT_DEFAULT = "openwrt.sh" SCRIPT_DEFAULT = "default.sh"

View File

@ -1,4 +1,5 @@
import falcon
import logging import logging
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -20,7 +21,7 @@ def whitelist_subnets(subnets):
req.env["PATH_INFO"], req.env["PATH_INFO"],
req.context.get("user", "unauthenticated user"), req.context.get("user", "unauthenticated user"),
req.context.get("remote_addr")) req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr"))
return func(self, req, resp, *args, **kwargs) return func(self, req, resp, *args, **kwargs)
return wrapped return wrapped
@ -39,3 +40,19 @@ def whitelist_content_types(*content_types):
return wrapped return wrapped
return wrapper return wrapper
def whitelist_subject(func):
def wrapped(self, req, resp, cn, *args, **kwargs):
from ipaddress import ip_address
from certidude import authority
from xattr import getxattr
try:
path, buf, cert = authority.get_signed(cn)
except IOError:
raise falcon.HTTPNotFound()
else:
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii")
if req.context.get("remote_addr") != ip_address(inner_address):
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr"))
return func(self, req, resp, cn, *args, **kwargs)
return wrapped

View File

@ -183,7 +183,8 @@ pre {
padding-bottom: 2px; padding-bottom: 2px;
} }
.tags .tag { .tags .tag,
.attributes .attribute {
display: inline; display: inline;
background-size: 24px; background-size: 24px;
background-position: 0 4px; background-position: 0 4px;
@ -195,6 +196,15 @@ pre {
white-space: nowrap; white-space: nowrap;
} }
.attribute {
opacity: 0.25;
}
.attribute:hover {
opacity: 1;
}
select { select {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@ -205,7 +215,8 @@ select {
} }
.icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); } .icon.tag,
.icon.attribute { background-image: url("../img/iconmonstr-tag-3.svg"); }
.icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); }
.icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); }
@ -227,5 +238,12 @@ select {
.icon.upload { background-image: url("../img/iconmonstr-upload-17.svg"); } .icon.upload { background-image: url("../img/iconmonstr-upload-17.svg"); }
.icon.dist,
.icon.kernel { background-image: url("../img/iconmonstr-linux-os-1.svg"); }
.icon.if { background-image: url("../img/iconmonstr-sitemap-5.svg"); }
.icon.cpu,
.icon.mem,
.icon.ram { background-image: url("../img/iconmonstr-cpu-1.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");}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 17c0 1.104-.896 2-2 2h-11c-1.104 0-2-.896-2-2v-11c0-1.104.896-2 2-2h11c1.104 0 2 .896 2 2v11zm-11 3v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm-8-20v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm4 15h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1zm-20 8h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1z"/></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20.581 19.049c-.55-.446-.336-1.431-.907-1.917.553-3.365-.997-6.331-2.845-8.232-1.551-1.595-1.051-3.147-1.051-4.49 0-2.146-.881-4.41-3.55-4.41-2.853 0-3.635 2.38-3.663 3.738-.068 3.262.659 4.11-1.25 6.484-2.246 2.793-2.577 5.579-2.07 7.057-.237.276-.557.582-1.155.835-1.652.72-.441 1.925-.898 2.78-.13.243-.192.497-.192.74 0 .75.596 1.399 1.679 1.302 1.461-.13 2.809.905 3.681.905.77 0 1.402-.438 1.696-1.041 1.377-.339 3.077-.296 4.453.059.247.691.917 1.141 1.662 1.141 1.631 0 1.945-1.849 3.816-2.475.674-.225 1.013-.879 1.013-1.488 0-.39-.139-.761-.419-.988zm-9.147-10.465c-.319 0-.583-.258-1-.568-.528-.392-1.065-.618-1.059-1.03 0-.283.379-.37.869-.681.526-.333.731-.671 1.249-.671.53 0 .69.268 1.41.579.708.307 1.201.427 1.201.773 0 .355-.741.609-1.158.868-.613.378-.928.73-1.512.73zm1.665-5.215c.882.141.981 1.691.559 2.454l-.355-.145c.184-.543.181-1.437-.435-1.494-.391-.036-.643.48-.697.922-.153-.064-.32-.11-.523-.127.062-.923.658-1.737 1.451-1.61zm-3.403.331c.676-.168 1.075.618 1.078 1.435l-.31.19c-.042-.343-.195-.897-.579-.779-.411.128-.344 1.083-.115 1.279l-.306.17c-.42-.707-.419-2.133.232-2.295zm-2.115 19.243c-1.963-.893-2.63-.69-3.005-.69-.777 0-1.031-.579-.739-1.127.248-.465.171-.952.11-1.343-.094-.599-.111-.794.478-1.052.815-.346 1.177-.791 1.447-1.124.758-.937 1.523.537 2.15 1.85.407.851 1.208 1.282 1.455 2.225.227.871-.71 1.801-1.896 1.261zm6.987-1.874c-1.384.673-3.147.982-4.466.299-.195-.563-.507-.927-.843-1.293.539-.142.939-.814.46-1.489-.511-.721-1.555-1.224-2.61-2.04-.987-.763-1.299-2.644.045-4.746-.655 1.862-.272 3.578.057 4.069.068-.988.146-2.638 1.496-4.615.681-.998.691-2.316.706-3.14l.62.424c.456.337.838.708 1.386.708.81 0 1.258-.466 1.882-.853.244-.15.613-.302.923-.513.52 2.476 2.674 5.454 2.795 7.15.501-1.032-.142-3.514-.142-3.514.842 1.285.909 2.356.946 3.67.589.241 1.221.869 1.279 1.696l-.245-.028c-.126-.919-2.607-2.269-2.83-.539-1.19.181-.757 2.066-.997 3.288-.11.559-.314 1.001-.462 1.466zm4.846-.041c-.985.38-1.65 1.187-2.107 1.688-.88.966-2.044.503-2.168-.401-.131-.966.36-1.493.572-2.574.193-.987-.023-2.506.431-2.668.295 1.753 2.066 1.016 2.47.538.657 0 .712.222.859.837.092.385.219.709.578 1.09.418.447.29 1.133-.635 1.49zm-8-13.006c-.651 0-1.138-.433-1.534-.769-.203-.171.05-.487.253-.315.387.328.777.675 1.281.675.607 0 1.142-.519 1.867-.805.247-.097.388.285.143.382-.704.277-1.269.832-2.01.832z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 6h-8v-6h8v6zm-10 12h-6v6h6v-6zm18 0h-6v6h6v-6zm-11-7v-3h-2v3h-9v5h2v-3h7v3h2v-3h7v3h2v-5h-9zm2 7h-6v6h6v-6z"/></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -181,6 +181,24 @@ function onTagUpdated(e) {
}) })
} }
function onAttributeUpdated(e) {
var cn = e.data;
console.log("Attributes updated", cn);
$.ajax({
method: "GET",
url: "/api/signed/" + cn + "/attr/",
dataType: "json",
success:function(attributes, status, xhr) {
console.info("Updated", cn, "attributes", attributes);
$(".attributes[data-cn='" + cn + "']").html(
nunjucks.render('views/attributes.html', {
certificate: {
common_name: cn,
attributes:attributes }}));
}
})
}
$(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({
@ -228,6 +246,7 @@ $(document).ready(function() {
source.addEventListener("request-signed", onRequestSigned); source.addEventListener("request-signed", onRequestSigned);
source.addEventListener("certificate-revoked", onCertificateRevoked); source.addEventListener("certificate-revoked", onCertificateRevoked);
source.addEventListener("tag-update", onTagUpdated); source.addEventListener("tag-update", onTagUpdated);
source.addEventListener("attribute-update", onAttributeUpdated);
console.info("Swtiching to requests section"); console.info("Swtiching to requests section");
$("section").hide(); $("section").hide();

View File

@ -0,0 +1,3 @@
{% for key, value in certificate.attributes %}
<span class="attribute icon {{ key | replace('.', ' ') }}">{{ value }}</span>
{% endfor %}

View File

@ -61,4 +61,8 @@
<div class="status"> <div class="status">
{% include 'views/status.html' %} {% include 'views/status.html' %}
</div> </div>
<div class="attributes" data-cn="{{ certificate.common_name }}">
{% include 'views/attributes.html' %}
</div>
</li> </li>

View File

@ -0,0 +1,15 @@
#!/bin/sh
# Tags:
{% for tag in tags %}
# {{ tag }}
{% endfor %}
curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\
dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\
dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\
kernel=$(uname -sr)&\
dist=$(lsb_release -si) $(lsb_release -sr)&\
cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\
mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\
$(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
# This script can executed on a preconfigured OpenWrt box # This script can be executed on a preconfigured OpenWrt box
# https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html # https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html
# Password protected wireless area # Password protected wireless area
@ -10,13 +10,13 @@ for band in 2ghz 5ghz; do
uci set wireless.lan$band.mode=ap uci set wireless.lan$band.mode=ap
uci set wireless.lan$band.device=radio$band uci set wireless.lan$band.device=radio$band
uci set wireless.lan$band.encryption=psk2 uci set wireless.lan$band.encryption=psk2
{% if attributes.protected and attributes.protected.ssid %} {% if attributes.wireless.protected and attributes.wireless.protected.ssid %}
uci set wireless.lan$band.ssid={{ attrbutes.protected.ssid }} uci set wireless.lan$band.ssid={{ attrbutes.wireless.protected.ssid }}
{% else %} {% else %}
uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected
{% endif %} {% endif %}
{% if attributes.protected and attributes.protected.psk %} {% if attributes.wireless.protected and attributes.wireless.protected.psk %}
uci set wireless.lan$band.key={{ attributes.protected.psk }} uci set wireless.lan$band.key={{ attributes.wireless.protected.psk }}
{% else %} {% else %}
uci set wireless.lan$band.key=salakala uci set wireless.lan$band.key=salakala
{% endif %} {% endif %}
@ -29,8 +29,8 @@ for band in 2ghz 5ghz; do
uci set wireless.guest$band.mode=ap uci set wireless.guest$band.mode=ap
uci set wireless.guest$band.device=radio$band uci set wireless.guest$band.device=radio$band
uci set wireless.guest$band.encryption=none uci set wireless.guest$band.encryption=none
{% if attributes.public and attributes.public.ssid %} {% if attributes.wireless.public and attributes.wireless.public.ssid %}
uci set wireless.guest$band.ssid={{ attrbutes.public.ssid }} uci set wireless.guest$band.ssid={{ attrbutes.wireless.public.ssid }}
{% else %} {% else %}
uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public
{% endif %} {% endif %}

View File

@ -23,7 +23,7 @@ server {
root {{static_path}}; root {{static_path}};
location /api/ { location /api/ {
proxy_pass http://127.0.1.1{% if port != 80 %}:{{ port }}{% endif %}/api/; proxy_pass http://127.0.1.1:8080/api/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 600; proxy_connect_timeout 600;

View File

@ -7,8 +7,7 @@ Environment=PYTHON_EGG_CACHE=/tmp/.cache
PIDFile=/run/certidude/server.pid PIDFile=/run/certidude/server.pid
ExecReload=/bin/kill -s HUP $MAINPID ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID ExecStop=/bin/kill -s TERM $MAINPID
ExecStart={{ certidude_path }} serve {% if listen %} -l {{listen}}{% endif %}{% if port %} -p {{port}}{% endif %} ExecStart={{ certidude_path }} serve
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target