Complete overhaul

* Switch to Python 2.x due to lack of decent LDAP support in Python 3.x
* Add LDAP backend for authentication/authorization
* Add PAM backend for authentication
* Add getent backend for authorization
* Add preliminary CSRF protection
* Update icons
* Update push server documentation, use nchan from now on
* Add P12 bundle generation
* Add thin wrapper around Python's SQL connectors
* Enable mailing subsystem
* Add Kerberos TGT renewal cronjob
* Add HTTPS server setup commands for nginx
This commit is contained in:
Lauri Võsandi 2016-03-21 23:42:39 +02:00
parent ffdab4d36d
commit 811e6dbb08
96 changed files with 2140 additions and 10312 deletions

6
.gitignore vendored
View File

@ -54,3 +54,9 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
# npm
node_modules/
# diff
*.diff

View File

@ -1,13 +1,11 @@
include README.rst include README.rst
include certidude/templates/*.sh
include certidude/templates/*.html
include certidude/templates/*.svg
include certidude/templates/*.ovpn include certidude/templates/*.ovpn
include certidude/templates/*.cnf
include certidude/templates/*.conf include certidude/templates/*.conf
include certidude/templates/*.ini include certidude/templates/*.ini
include certidude/templates/mail/*.md
include certidude/static/js/*.js include certidude/static/js/*.js
include certidude/static/css/*.css include certidude/static/css/*.css
include certidude/static/fonts/*.woff2 include certidude/static/fonts/*.woff2
include certidude/static/img/*.svg include certidude/static/img/*.svg
include certidude/static/*.html include certidude/static/*.html
include certidude/sql/*/*.sql

View File

@ -67,8 +67,11 @@ To install Certidude:
.. code:: bash .. code:: bash
apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev libkrb5-dev apt-get install -y python python-pip python-dev cython \
pip3 install --allow-external mysql-connector-python mysql-connector-python python-pysqlite2 python-mysql.connector python-ldap \
build-essential libffi-dev libssl-dev libkrb5-dev \
ldap-utils krb5-user default-mta \
libsasl2-modules-gssapi-mit
pip3 install certidude pip3 install certidude
Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI,
@ -79,6 +82,7 @@ Create a system user for ``certidude``:
.. code:: bash .. code:: bash
adduser --system --no-create-home --group certidude adduser --system --no-create-home --group certidude
mkdir /etc/certidude
Setting up CA Setting up CA
@ -90,7 +94,7 @@ You can check it with:
.. code:: bash .. code:: bash
hostname -f hostname -f
The command should return ca.example.co The command should return ca.example.co
@ -144,7 +148,7 @@ Install ``nginx`` and ``uwsgi``:
.. code:: bash .. code:: bash
apt-get install nginx uwsgi uwsgi-plugin-python3 apt-get install nginx uwsgi uwsgi-plugin-python
For easy setup following is reccommended: For easy setup following is reccommended:
@ -162,7 +166,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
vaccum = true vaccum = true
uid = certidude uid = certidude
gid = certidude gid = certidude
plugins = python34 plugins = python
chdir = /tmp chdir = /tmp
module = certidude.wsgi module = certidude.wsgi
callable = app callable = app
@ -192,7 +196,7 @@ configure the site in /etc/nginx/sites-available/certidude:
server_name localhost; server_name localhost;
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server ipv6only=on; listen [::]:80 default_server ipv6only=on;
root /usr/local/lib/python3.4/dist-packages/certidude/static; root /usr/local/lib/python2.7/dist-packages/certidude/static;
location /api/ { location /api/ {
include uwsgi_params; include uwsgi_params;
@ -201,19 +205,20 @@ configure the site in /etc/nginx/sites-available/certidude:
# Add following three if you wish to enable push server on this machine # Add following three if you wish to enable push server on this machine
location /pub { location /pub {
allow 127.0.0.1; # Allow publishing only from CA machine allow 127.0.0.1;
push_stream_publisher admin; nchan_publisher http;
push_stream_channels_path $arg_id; nchan_store_messages off;
nchan_channel_id $arg_id;
} }
location ~ "^/lp/(.*)" { location ~ "^/lp/(.*)" {
push_stream_channels_path $1; nchan_subscriber longpoll;
push_stream_subscriber long-polling; nchan_channel_id $1;
} }
location ~ "^/ev/(.*)" { location ~ "^/ev/(.*)" {
push_stream_channels_path $1; nchan_subscriber eventsource;
push_stream_subscriber eventsource; nchan_channel_id $1;
} }
} }
@ -254,6 +259,8 @@ Also adjust ``/etc/nginx/nginx.conf``:
In your CA ssl.cnf make sure Certidude is aware of your nginx setup: In your CA ssl.cnf make sure Certidude is aware of your nginx setup:
.. code::
push_server = http://push.example.com/ push_server = http://push.example.com/
Restart the services: Restart the services:
@ -283,7 +290,7 @@ Make sure Certidude machine's fully qualified hostname is correct in ``/etc/host
127.0.0.1 localhost 127.0.0.1 localhost
127.0.1.1 ca.example.lan ca 127.0.1.1 ca.example.lan ca
Set up Samba client configuration in ``/etc/samba/smb.conf``: Reset Samba client configuration in ``/etc/samba/smb.conf``:
.. code:: ini .. code:: ini
@ -294,11 +301,36 @@ Set up Samba client configuration in ``/etc/samba/smb.conf``:
realm = EXAMPLE.LAN realm = EXAMPLE.LAN
kerberos method = system keytab kerberos method = system keytab
Reset Kerberos configuration in ``/etc/krb5.conf``:
.. code:: ini
[libdefaults]
default_realm = EXAMPLE.LAN
dns_lookup_realm = true
dns_lookup_kdc = true
forwardable = true
proxiable = true
Initialize Kerberos credentials:
.. code:: bash
kinit administrator
Join the machine to domain:
.. code:: bash
net ads join -k
Set up Kerberos keytab for the web service: Set up Kerberos keytab for the web service:
.. code:: bash .. code:: bash
KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -U Administrator KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k
chown root:certidude /etc/certidude/server.keytab
chmod 640 /etc/certidude/server.keytab
Setting up authorization Setting up authorization
@ -379,22 +411,29 @@ Clone the repository:
git clone https://github.com/laurivosandi/certidude git clone https://github.com/laurivosandi/certidude
cd certidude cd certidude
Install dependencies as shown above and additionally:
.. code:: bash
pip install -r requirements.txt
To generate templates: To generate templates:
.. code:: bash .. code:: bash
apt-get install npm nodejs apt-get install npm nodejs
npm install nunjucks sudo ln -s nodejs /usr/bin/node # Fix 'env node' on Ubuntu 14.04
nunjucks-precompile --include "\\.html$" --include "\\.svg" certidude/static/ > certidude/static/js/templates.js npm install -g nunjucks
nunjucks-precompile --include "\\.html$" --include "\\.svg$" certidude/static/ > certidude/static/js/templates.js
To run from source tree: To run from source tree:
.. code:: bash .. code:: bash
PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python3 misc/certidude PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude
To install the package from the source: To install the package from the source:
.. code:: bash .. code:: bash
python3 setup.py install --single-version-externally-managed --root / python setup.py install --single-version-externally-managed --root /

View File

@ -1,13 +1,19 @@
# encoding: utf-8
import falcon import falcon
import mimetypes import mimetypes
import logging
import os import os
import click import click
from datetime import datetime
from time import sleep from time import sleep
from certidude import authority from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize, event_source from certidude.decorators import serialize, event_source, csrf_protection
from certidude.wrappers import Request, Certificate from certidude.wrappers import Request, Certificate
from certidude import config from certidude import constants, config
logger = logging.getLogger("api")
class CertificateStatusResource(object): class CertificateStatusResource(object):
""" """
@ -24,7 +30,9 @@ class CertificateStatusResource(object):
class CertificateAuthorityResource(object): class CertificateAuthorityResource(object):
def on_get(self, req, resp): def on_get(self, req, resp):
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
resp.append_header("Content-Type", "application/x-x509-ca-cert")
resp.append_header("Content-Disposition", "attachment; filename=ca.crt") resp.append_header("Content-Disposition", "attachment; filename=ca.crt")
@ -34,16 +42,54 @@ class SessionResource(object):
@authorize_admin @authorize_admin
@event_source @event_source
def on_get(self, req, resp): def on_get(self, req, resp):
if config.ACCOUNTS_BACKEND == "ldap":
import ldap
ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*")
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE,
ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"])
for dn,entry in r:
cn, = entry.get("cn")
break
else:
raise ValueError("Failed to look up group %s in LDAP" % repr(group_name))
admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")])
elif config.ACCOUNTS_BACKEND == "posix":
import grp
_, _, gid, members = grp.getgrnam(config.ADMINS_GROUP)
admins = dict([(j, j) for j in members])
else:
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
return dict( return dict(
username=req.context.get("user"), user = dict(
event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, name=req.context.get("user").name,
gn=req.context.get("user").given_name,
sn=req.context.get("user").surname,
mail=req.context.get("user").mail
),
request_submission_allowed = sum( # Dirty hack!
[req.context.get("remote_addr") in j
for j in config.REQUEST_SUBNETS]),
user_subnets = config.USER_SUBNETS,
autosign_subnets = config.AUTOSIGN_SUBNETS, autosign_subnets = config.AUTOSIGN_SUBNETS,
request_subnets = config.REQUEST_SUBNETS, request_subnets = config.REQUEST_SUBNETS,
admin_subnets=config.ADMIN_SUBNETS, admin_subnets=config.ADMIN_SUBNETS,
admin_users=config.ADMIN_USERS, admin_users = admins,
requests=authority.list_requests(), #admin_users=config.ADMIN_USERS,
signed=authority.list_signed(), authority = dict(
revoked=authority.list_revoked()) outbox = config.OUTBOX,
certificate = authority.certificate,
events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN,
requests=authority.list_requests(),
signed=authority.list_signed(),
revoked=authority.list_revoked(),
) if config.ADMINS_GROUP in req.context.get("groups") else None,
features=dict(
tagging=config.TAGGING_BACKEND,
leases=False, #config.LEASES_BACKEND,
logging=config.LOGGING_BACKEND))
class StaticResource(object): class StaticResource(object):
@ -58,7 +104,7 @@ class StaticResource(object):
if os.path.isdir(path): if os.path.isdir(path):
path = os.path.join(path, "index.html") path = os.path.join(path, "index.html")
print("Serving:", path) click.echo("Serving: %s" % 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)
@ -72,7 +118,33 @@ class StaticResource(object):
resp.body = "File '%s' not found" % req.path resp.body = "File '%s' not found" % req.path
class BundleResource(object):
@login_required
def on_get(self, req, resp):
common_name = req.context["user"].mail
logger.info("Signing bundle %s for %s", common_name, req.context.get("user"))
resp.set_header("Content-Type", "application/x-pkcs12")
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name)
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user"))
import ipaddress
class NormalizeMiddleware(object):
@csrf_protection
def process_request(self, req, resp, *args):
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"))
def process_response(self, req, resp, resource):
# wtf falcon?!
if isinstance(resp.location, unicode):
resp.location = resp.location.encode("ascii")
def certidude_app(): def certidude_app():
from certidude import config
from .revoked import RevocationListResource from .revoked import RevocationListResource
from .signed import SignedCertificateListResource, SignedCertificateDetailResource from .signed import SignedCertificateListResource, SignedCertificateDetailResource
from .request import RequestListResource, RequestDetailResource from .request import RequestListResource, RequestDetailResource
@ -82,60 +154,56 @@ def certidude_app():
from .tag import TagResource, TagDetailResource from .tag import TagResource, TagDetailResource
from .cfg import ConfigResource, ScriptResource from .cfg import ConfigResource, ScriptResource
app = falcon.API() app = falcon.API(middleware=NormalizeMiddleware())
# Certificate authority API calls # Certificate authority API calls
app.add_route("/api/ocsp/", CertificateStatusResource()) app.add_route("/api/ocsp/", CertificateStatusResource())
app.add_route("/api/bundle/", BundleResource())
app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/certificate/", CertificateAuthorityResource())
app.add_route("/api/revoked/", RevocationListResource()) app.add_route("/api/revoked/", RevocationListResource())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
app.add_route("/api/signed/", SignedCertificateListResource()) app.add_route("/api/signed/", SignedCertificateListResource())
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/tag/", TagResource())
app.add_route("/api/tag/{identifier}/", TagDetailResource())
app.add_route("/api/config/", ConfigResource())
app.add_route("/api/script/", ScriptResource())
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?
app.add_route("/api/lease/", LeaseResource()) app.add_route("/api/lease/", LeaseResource())
app.add_route("/api/whois/", WhoisResource()) app.add_route("/api/whois/", WhoisResource())
""" log_handlers = []
Set up logging if config.LOGGING_BACKEND == "sql":
""" from certidude.mysqllog import LogHandler
uri = config.cp.get("logging", "database")
log_handlers.append(LogHandler(uri))
app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog":
from logging.handlers import SyslogHandler
log_handlers.append(SysLogHandler())
# Browsing syslog via HTTP is obviously not possible out of the box
elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
from certidude import config if config.TAGGING_BACKEND == "sql":
from certidude.mysqllog import MySQLLogHandler uri = config.cp.get("tagging", "database")
from datetime import datetime app.add_route("/api/tag/", TagResource(uri))
import logging app.add_route("/api/tag/{identifier}/", TagDetailResource(uri))
import socket app.add_route("/api/config/", ConfigResource(uri))
import json app.add_route("/api/script/", ScriptResource(uri))
elif config.TAGGING_BACKEND:
raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND)
if config.PUSH_PUBLISH:
class PushLogHandler(logging.Handler): from certidude.push import PushLogHandler
def emit(self, record): log_handlers.append(PushLogHandler())
from certidude.push import publish
publish("log-entry", dict(
created = datetime.fromtimestamp(record.created),
message = record.msg % record.args,
severity = record.levelname.lower()))
if config.DATABASE_POOL:
sql_handler = MySQLLogHandler(config.DATABASE_POOL)
push_handler = PushLogHandler()
for facility in "api", "cli": for facility in "api", "cli":
logger = logging.getLogger(facility) logger = logging.getLogger(facility)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if config.DATABASE_POOL: for handler in log_handlers:
logger.addHandler(sql_handler) logger.addHandler(handler)
logger.addHandler(push_handler)
logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN)
logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN)
import atexit import atexit

View File

@ -6,6 +6,7 @@ from random import choice
from certidude import config from certidude import config
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
from certidude.relational import RelationalMixin
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -39,43 +40,42 @@ where
device.cn = %s device.cn = %s
""" """
SQL_SELECT_INHERITANCE = """
SQL_SELECT_RULES = """
select select
tag_inheritance.`id` as `id`, tag.cn as `cn`,
tag.id as `tag_id`, tag.key as `tag_key`,
tag.`key` as `match_key`, tag.value as `tag_value`,
tag.`value` as `match_value`, tag_properties.property_key as `property_key`,
tag_inheritance.`key` as `key`, tag_properties.property_value as `property_value`
tag_inheritance.`value` as `value` from
from tag_inheritance tag_properties
join tag on tag.id = tag_inheritance.tag_id join
tag
on
tag.key = tag_properties.tag_key and
tag.value = tag_properties.tag_value
""" """
class ConfigResource(object):
class ConfigResource(RelationalMixin):
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection() return self.iterfetch(SQL_SELECT_RULES)
cursor = conn.cursor(dictionary=True)
cursor.execute(SQL_SELECT_INHERITANCE)
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return g()
class ScriptResource(object):
class ScriptResource(RelationalMixin):
def on_get(self, req, resp): def on_get(self, req, resp):
from certidude.api.whois import address_to_identity from certidude.api.whois import address_to_identity
node = address_to_identity( node = address_to_identity(
config.DATABASE_POOL.get_connection(), self.connect(),
ipaddress.ip_address(req.env["REMOTE_ADDR"]) req.context.get("remote_addr")
) )
if not node: if not node:
resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"] resp.body = "Could not map IP address: %s" % req.context.get("remote_addr")
resp.status = falcon.HTTP_404 resp.status = falcon.HTTP_404
return return
@ -84,7 +84,7 @@ class ScriptResource(object):
key, common_name = identity.split("=") key, common_name = identity.split("=")
assert "=" not in common_name assert "=" not in common_name
conn = config.DATABASE_POOL.get_connection() conn = self.connect()
cursor = conn.cursor() cursor = conn.cursor()
resp.set_header("Content-Type", "text/x-shellscript") resp.set_header("Content-Type", "text/x-shellscript")

View File

@ -2,38 +2,14 @@
from certidude import config from certidude import config
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
from certidude.relational import RelationalMixin
class LogResource(RelationalMixin):
SQL_CREATE_TABLES = "log_tables.sql"
class LogResource(object):
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
""" # TODO: Add last id parameter
Translate currently online client's IP-address to distinguished name return self.iterfetch("select * from log order by created desc")
"""
SQL_LOG_ENTRIES = """
SELECT
*
FROM
log
ORDER BY created DESC
"""
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute(SQL_LOG_ENTRIES)
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return tuple(g())
# for acquired, released, identity in cursor:
# return {
# "acquired": datetime.utcfromtimestamp(acquired),
# "identity": parse_dn(bytes(identity))
# }
# return None

View File

@ -5,45 +5,42 @@ import logging
import ipaddress import ipaddress
import os import os
from certidude import config, authority, helpers, push, errors from certidude import config, authority, helpers, push, errors
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.wrappers import Request, Certificate from certidude.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types
logger = logging.getLogger("api") logger = logging.getLogger("api")
class RequestListResource(object): class RequestListResource(object):
@serialize @serialize
@login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
return helpers.list_requests() return authority.list_requests()
@login_optional
@whitelist_subnets(config.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10")
def on_post(self, req, resp): def on_post(self, req, resp):
""" """
Submit certificate signing request (CSR) in PEM format Submit certificate signing request (CSR) in PEM format
""" """
# Parse remote IPv4/IPv6 address
remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8"))
# Check for CSR submission whitelist body = req.stream.read(req.content_length)
if config.REQUEST_SUBNETS:
for subnet in config.REQUEST_SUBNETS:
if subnet.overlaps(remote_addr):
break
else:
logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr)
raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr)
if req.get_header("Content-Type") != "application/pkcs10":
raise falcon.HTTPUnsupportedMediaType(
"This API call accepts only application/pkcs10 content type")
body = req.stream.read(req.content_length).decode("ascii")
csr = Request(body) csr = Request(body)
if not csr.common_name:
logger.warning("Rejected signing request without common name from %s",
req.context.get("remote_addr"))
raise falcon.HTTPBadRequest(
"Bad request",
"No common name specified!")
# Check if this request has been already signed and return corresponding certificte if it has been signed # Check if this request has been already signed and return corresponding certificte if it has been signed
try: try:
cert = authority.get_signed(csr.common_name) cert = authority.get_signed(csr.common_name)
except FileNotFoundError: except EnvironmentError:
pass pass
else: else:
if cert.pubkey == csr.pubkey: if cert.pubkey == csr.pubkey:
@ -56,12 +53,12 @@ class RequestListResource(object):
# Process automatic signing if the IP address is whitelisted and autosigning was requested # Process automatic signing if the IP address is whitelisted and autosigning was requested
if req.get_param_as_bool("autosign"): if req.get_param_as_bool("autosign"):
for subnet in config.AUTOSIGN_SUBNETS: for subnet in config.AUTOSIGN_SUBNETS:
if subnet.overlaps(remote_addr): if subnet.overlaps(req.context.get("remote_addr")):
try: try:
resp.set_header("Content-Type", "application/x-x509-user-cert") resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr).dump() resp.body = authority.sign(csr).dump()
return return
except FileExistsError: # Certificate already exists, try to save the request except EnvironmentError: # Certificate already exists, try to save the request
pass pass
break break
@ -73,7 +70,8 @@ class RequestListResource(object):
pass pass
except errors.DuplicateCommonNameError: except errors.DuplicateCommonNameError:
# TODO: Certificate renewal # TODO: Certificate renewal
logger.warning("Rejected signing request with overlapping common name from %s", req.env["REMOTE_ADDR"]) logger.warning("Rejected signing request with overlapping common name from %s",
req.context.get("remote_addr"))
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")
@ -86,12 +84,12 @@ class RequestListResource(object):
url = config.PUSH_LONG_POLL % csr.fingerprint() url = config.PUSH_LONG_POLL % csr.fingerprint()
click.echo("Redirecting to: %s" % url) click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url) resp.set_header("Location", url.encode("ascii"))
logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url) logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
else: else:
# Request was accepted, but not processed # Request was accepted, but not processed
resp.status = falcon.HTTP_202 resp.status = falcon.HTTP_202
logger.info("Signing request from %s stored", req.env["REMOTE_ADDR"]) logger.info("Signing request from %s stored", req.context.get("remote_addr"))
class RequestDetailResource(object): class RequestDetailResource(object):
@ -101,11 +99,8 @@ class RequestDetailResource(object):
Fetch certificate signing request as PEM Fetch certificate signing request as PEM
""" """
csr = authority.get_request(cn) csr = authority.get_request(cn)
# if not os.path.exists(path): logger.debug("Signing request %s was downloaded by %s",
# raise falcon.HTTPNotFound() csr.common_name, req.context.get("remote_addr"))
resp.set_header("Content-Type", "application/pkcs10")
resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name)
return csr return csr
@login_required @login_required
@ -120,14 +115,17 @@ class RequestDetailResource(object):
resp.body = "Certificate successfully signed" resp.body = "Certificate successfully signed"
resp.status = falcon.HTTP_201 resp.status = falcon.HTTP_201
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context["user"], req.env["REMOTE_ADDR"]) logger.info("Signing request %s signed by %s from %s", csr.common_name,
req.context.get("user"), req.context.get("remote_addr"))
@login_required @login_required
@authorize_admin @authorize_admin
def on_delete(self, req, resp, cn): def on_delete(self, req, resp, cn):
try: try:
authority.delete_request(cn) authority.delete_request(cn)
except FileNotFoundError: # Logging implemented in the function above
except EnvironmentError as e:
resp.body = "No certificate CN=%s found" % cn resp.body = "No certificate CN=%s found" % cn
logger.warning("User %s attempted to delete non-existant signing request %s from %s", req.context["user"], cn, req.env["REMOTE_ADDR"]) logger.warning("User %s failed to delete signing request %s from %s, reason: %s",
req.context["user"], cn, req.context.get("remote_addr"), e)
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()

View File

@ -1,9 +1,12 @@
import logging
from certidude.authority import export_crl from certidude.authority import export_crl
logger = logging.getLogger("api")
class RevocationListResource(object): class RevocationListResource(object):
def on_get(self, req, resp): def on_get(self, req, resp):
logger.debug("Revocation list requested by %s", req.context.get("remote_addr"))
resp.set_header("Content-Type", "application/x-pkcs7-crl") resp.set_header("Content-Type", "application/x-pkcs7-crl")
resp.append_header("Content-Disposition", "attachment; filename=ca.crl") resp.append_header("Content-Disposition", "attachment; filename=ca.crl")
resp.body = export_crl() resp.body = export_crl()

View File

@ -9,40 +9,35 @@ logger = logging.getLogger("api")
class SignedCertificateListResource(object): class SignedCertificateListResource(object):
@serialize @serialize
@login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
for j in authority.list_signed(): return {"signed":authority.list_signed()}
yield omit(
key_type=j.key_type,
key_length=j.key_length,
identity=j.identity,
cn=j.common_name,
c=j.country_code,
st=j.state_or_county,
l=j.city,
o=j.organization,
ou=j.organizational_unit,
fingerprint=j.fingerprint())
class SignedCertificateDetailResource(object): class SignedCertificateDetailResource(object):
@serialize @serialize
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
# Compensate for NTP lag # Compensate for NTP lag
from time import sleep # from time import sleep
sleep(5) # sleep(5)
try: try:
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"]) cert = authority.get_signed(cn)
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn) except EnvironmentError:
return authority.get_signed(cn) logger.warning("Failed to serve non-existant certificate %s to %s",
except FileNotFoundError: cn, req.context.get("remote_addr"))
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()
else:
logger.debug("Served certificate %s to %s",
cn, req.context.get("remote_addr"))
return cert
@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"]) logger.info("Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr"))
authority.revoke_certificate(cn) authority.revoke_certificate(cn)

View File

@ -1,117 +1,63 @@
import falcon import falcon
import logging import logging
from certidude import config from certidude.relational import RelationalMixin
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") logger = logging.getLogger("api")
SQL_TAG_LIST = """ class TagResource(RelationalMixin):
select SQL_CREATE_TABLES = "tag_tables.sql"
device_tag.id as `id`,
tag.key as `key`,
tag.value as `value`,
device.cn as `cn`
from
device_tag
join
tag
on
device_tag.tag_id = tag.id
join
device
on
device_tag.device_id = device.id
"""
SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s"
class TagResource(object):
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection() return self.iterfetch("select * from tag")
cursor = conn.cursor(dictionary=True)
cursor.execute(SQL_TAG_LIST)
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return tuple(g())
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_post(self, req, resp): def on_post(self, req, resp):
from certidude import push from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
args = req.get_param("cn"),
cursor.execute(
"insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args)
device_id = cursor.lastrowid
args = req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
tag_id = cursor.lastrowid
args = device_id, tag_id
cursor.execute(
"insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args)
push.publish("tag-added", str(cursor.lastrowid))
args = req.get_param("cn"), req.get_param("key"), req.get_param("value") args = req.get_param("cn"), req.get_param("key"), req.get_param("value")
rowid = self.sql_execute("tag_insert.sql", *args)
push.publish("tag-added", str(rowid))
logger.debug("Tag cn=%s, key=%s, value=%s added" % args) logger.debug("Tag cn=%s, key=%s, value=%s added" % args)
conn.commit()
cursor.close()
conn.close()
class TagDetailResource(object): class TagDetailResource(RelationalMixin):
SQL_CREATE_TABLES = "tag_tables.sql"
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp, identifier): def on_get(self, req, resp, identifier):
conn = config.DATABASE_POOL.get_connection() conn = self.sql_connect()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor()
cursor.execute(SQL_TAG_DETAIL, (identifier,)) if self.uri.scheme == "mysql":
cursor.execute("select `cn`, `key`, `value` from tag where id = %s", (identifier,))
else:
cursor.execute("select `cn`, `key`, `value` from tag where id = ?", (identifier,))
cols = [j[0] for j in cursor.description]
for row in cursor: for row in cursor:
cursor.close() cursor.close()
conn.close() conn.close()
return row return dict(zip(cols, row))
cursor.close() cursor.close()
conn.close() conn.close()
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_put(self, req, resp, identifier): def on_put(self, req, resp, identifier):
from certidude import push from certidude import push
conn = config.DATABASE_POOL.get_connection() args = req.get_param("value"), identifier
cursor = conn.cursor() self.sql_execute("tag_update.sql", *args)
# Create tag if necessary
args = req.get_param("key"), req.get_param("value")
cursor.execute(
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
tag_id = cursor.lastrowid
# Attach tag to device
cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1",
(tag_id, identifier))
conn.commit()
cursor.close()
conn.close()
logger.debug("Tag %s updated, value set to %s", logger.debug("Tag %s updated, value set to %s",
identifier, req.get_param("value")) identifier, req.get_param("value"))
push.publish("tag-updated", identifier) push.publish("tag-updated", identifier)
@ -122,13 +68,6 @@ class TagDetailResource(object):
@authorize_admin @authorize_admin
def on_delete(self, req, resp, identifier): def on_delete(self, req, resp, identifier):
from certidude import push from certidude import push
conn = config.DATABASE_POOL.get_connection() self.sql_execute("tag_delete.sql", identifier)
cursor = conn.cursor()
cursor.execute("delete from device_tag where id = %s", (identifier,))
conn.commit()
cursor.close()
conn.close()
push.publish("tag-removed", identifier) push.publish("tag-removed", identifier)
logger.debug("Tag %s removed" % identifier) logger.debug("Tag %s removed" % identifier)

View File

@ -46,7 +46,7 @@ class WhoisResource(object):
identity = address_to_identity( identity = address_to_identity(
conn, conn,
ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) req.context.get("remote_addr")
) )
conn.close() conn.close()
@ -55,4 +55,4 @@ class WhoisResource(object):
return dict(address=identity[0], acquired=identity[1], identity=identity[2]) return dict(address=identity[0], acquired=identity[1], identity=identity[2])
else: else:
resp.status = falcon.HTTP_403 resp.status = falcon.HTTP_403
resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"] resp.body = "Failed to look up node %s" % req.context.get("remote_addr")

View File

@ -1,144 +1,324 @@
import click import click
import falcon import falcon
import ipaddress
import kerberos import kerberos
import logging import logging
import os import os
import re import re
import socket import socket
from certidude import config from certidude.firewall import whitelist_subnets
from certidude import config, constants
logger = logging.getLogger("api") logger = logging.getLogger("api")
# Vanilla Kerberos provides only username.
# AD also embeds PAC (Privilege Attribute Certificate), which
# is supposed to be sent via HTTP headers and it contains
# the groups user is part of.
# Even then we would have to manually look up the e-mail
# address eg via LDAP, hence to keep things simple
# we simply use Kerberos to authenticate.
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
if config.AUTHENTICATION_BACKEND == "kerberos": if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
if not os.getenv("KRB5_KTNAME"): ktname = os.getenv("KRB5_KTNAME")
if not ktname:
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
exit(250) exit(250)
if not os.path.exists(ktname):
click.echo("Kerberos keytab %s does not exist" % ktname, err=True)
exit(248)
try: try:
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
except kerberos.KrbError as exc: except kerberos.KrbError as exc:
click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True)
exit(249) exit(249)
else: else:
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
else:
NotImplemented
def login_required(func):
def pam_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against PAM with WWW Basic Auth credentials
"""
authorization = req.get_header("Authorization")
if not authorization:
resp.append_header("WWW-Authenticate", "Basic")
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate")
if not authorization.startswith("Basic "): class User(object):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization) def __init__(self, name):
if "@" in name:
self.mail = name
self.name, self.domain = name.split("@")
else:
self.mail = None
self.name, self.domain = name, None
self.given_name, self.surname = None, None
from base64 import b64decode def __repr__(self):
basic, token = authorization.split(" ", 1) if self.given_name and self.surname:
user, passwd = b64decode(token).split(":", 1) return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
else:
return self.mail
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
raise falcon.HTTPForbidden("Forbidden", "Invalid password")
req.context["user"] = user def member_of(group_name):
"""
Check if requesting user is member of an UNIX group
"""
def wrapper(func):
def posix_check_group_membership(resource, req, resp, *args, **kwargs):
import grp
_, _, gid, members = grp.getgrnam(group_name)
if req.context.get("user").name not in members:
logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name)
raise falcon.HTTPForbidden("Forbidden", "User not member of designated group")
req.context.get("groups").add(group_name)
return func(resource, req, resp, *args, **kwargs)
def ldap_check_group_membership(resource, req, resp, *args, **kwargs):
import ldap
ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn)
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"),
["member"])
for dn,entry in r:
if not dn: continue
logger.debug("User %s is member of group %s" % (
req.context.get("user"), repr(group_name)))
req.context.get("groups").add(group_name)
break
else:
raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name))
return func(resource, req, resp, *args, **kwargs)
if config.AUTHORIZATION_BACKEND == "ldap":
return ldap_check_group_membership
elif config.AUTHORIZATION_BACKEND == "posix":
return posix_check_group_membership
else:
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
return wrapper
def account_info(func):
# TODO: Use Privilege Account Certificate for Kerberos
def posix_account_info(resource, req, resp, *args, **kwargs):
import pwd
_, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name)
gecos = gecos.decode("utf-8").split(",")
full_name = gecos[0]
if full_name and " " in full_name:
req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1)
req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN
return func(resource, req, resp, *args, **kwargs) return func(resource, req, resp, *args, **kwargs)
def ldap_account_info(resource, req, resp, *args, **kwargs):
import ldap
import ldap.sasl
def kerberos_authenticate(resource, req, resp, *args, **kwargs): if "ldap_conn" not in req.context:
authorization = req.get_header("Authorization") for server in config.LDAP_SERVERS:
conn = ldap.initialize(server)
conn.set_option(ldap.OPT_REFERRALS, 0)
if os.path.exists("/etc/krb5.keytab"):
ticket_cache = os.getenv("KRB5CCNAME")
if not ticket_cache:
raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!")
click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache))
conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
else:
raise NotImplementedError("LDAP simple bind not supported, use Kerberos")
req.context["ldap_conn"] = conn
break
else:
raise ValueError("No LDAP servers!")
if not authorization: ft = config.LDAP_USER_FILTER % req.context.get("user").name
resp.append_header("WWW-Authenticate", "Negotiate") r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
logger.debug("No Kerberos ticket offered while attempting to access %s from %s", req.env["PATH_INFO"], req.env["REMOTE_ADDR"]) ft,
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") ["cn", "givenname", "sn", "mail", "userPrincipalName"])
token = ''.join(authorization.split()[1:]) for dn, entry in r:
if not dn: continue
if entry.get("givenname") and entry.get("sn"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")
req.context["user"].given_name = given_name.decode("utf-8")
req.context["user"].surname = surname.decode("utf-8")
else:
cn, = entry.get("cn")
if " " in cn:
req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1)
try: req.context["user"].dn = dn.decode("utf-8")
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,)
except kerberos.GSSError as ex: retval = func(resource, req, resp, *args, **kwargs)
# TODO: logger.error req.context.get("ldap_conn").unbind_s()
raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],)) return retval
try:
result = kerberos.authGSSServerStep(context, token)
except kerberos.GSSError as ex:
s = str(dir(ex))
kerberos.authGSSServerClean(context)
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%s)" % (ex.args[0][0], ex.args[1][0]))
except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context)
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context)
req.context["user"], req.context["user_realm"] = user.split("@")
try:
# BUGBUG: https://github.com/02strich/pykerberos/issues/6
#kerberos.authGSSServerClean(context)
pass
except kerberos.GSSError as ex:
# TODO: logger.error
raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],))
if result == kerberos.AUTH_GSS_COMPLETE:
logger.debug("Succesfully authenticated user %s for %s from %s", req.context["user"], req.env["PATH_INFO"], req.env["REMOTE_ADDR"])
return func(resource, req, resp, *args, **kwargs)
elif result == kerberos.AUTH_GSS_CONTINUE:
# TODO: logger.error
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
else: else:
# TODO: logger.error raise ValueError("Failed to look up %s in LDAP" % req.context.get("user"))
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
if config.AUTHENTICATION_BACKEND == "kerberos": if config.ACCOUNTS_BACKEND == "ldap":
return kerberos_authenticate return ldap_account_info
elif config.AUTHENTICATION_BACKEND == "pam": elif config.ACCOUNTS_BACKEND == "posix":
return pam_authenticate return posix_account_info
else: else:
NotImplemented raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND)
def authenticate(optional=False):
def wrapper(func):
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
if not req.auth:
resp.append_header("WWW-Authenticate", "Negotiate")
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
req.env["PATH_INFO"], req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized",
"No Kerberos ticket offered, are you sure you've logged in with domain user account?")
token = ''.join(req.auth.split()[1:])
try:
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
except kerberos.GSSError as ex:
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
"Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],))
try:
result = kerberos.authGSSServerStep(context, token)
except kerberos.GSSError as ex:
kerberos.authGSSServerClean(context)
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1]))
except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context)
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context)
req.context["user"] = User(user)
req.context["groups"] = set()
try:
kerberos.authGSSServerClean(context)
except kerberos.GSSError as ex:
# TODO: logger.error
raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0]))
if result == kerberos.AUTH_GSS_COMPLETE:
logger.debug("Succesfully authenticated user %s for %s from %s",
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
return account_info(func)(resource, req, resp, *args, **kwargs)
elif result == kerberos.AUTH_GSS_CONTINUE:
# TODO: logger.error
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
else:
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
def ldap_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against LDAP with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
import ldap
if not req.auth:
resp.append_header("WWW-Authenticate", "Basic")
raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
if not req.auth.startswith("Basic "):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
from base64 import b64decode
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).split(":", 1)
if "ldap_conn" not in req.context:
for server in config.LDAP_SERVERS:
click.echo("Connecting to %s as %s" % (server, user))
conn = ldap.initialize(server)
conn.set_option(ldap.OPT_REFERRALS, 0)
try:
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
except ldap.LDAPError, e:
resp.append_header("WWW-Authenticate", "Basic")
logger.debug("Failed to authenticate with user '%s'", user)
raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
req.context["ldap_conn"] = conn
break
else:
raise ValueError("No LDAP servers!")
req.context["user"] = User(user)
req.context["groups"] = set()
return account_info(func)(resource, req, resp, *args, **kwargs)
def pam_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against PAM with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
if not req.auth:
resp.append_header("WWW-Authenticate", "Basic")
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate")
if not req.auth.startswith("Basic "):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
from base64 import b64decode
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).split(":", 1)
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
req.context["user"] = User(user)
req.context["groups"] = set()
return account_info(func)(resource, req, resp, *args, **kwargs)
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
return kerberos_authenticate
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
return pam_authenticate
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
return ldap_authenticate
else:
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
return wrapper
def login_required(func):
return authenticate()(func)
def login_optional(func):
return authenticate(optional=True)(func)
def authorize_admin(func): def authorize_admin(func):
def wrapped(self, req, resp, *args, **kwargs):
from certidude import config
# Parse remote IPv4/IPv6 address
remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8"))
# Check for administration subnet whitelist
print("Comparing:", config.ADMIN_SUBNETS, "To:", remote_addr)
for subnet in config.ADMIN_SUBNETS:
if subnet.overlaps(remote_addr):
break
else:
logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr)
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr)
def whitelist_authorize(resource, req, resp, *args, **kwargs):
# Check for username whitelist # Check for username whitelist
if req.context.get("user") not in config.ADMIN_USERS: if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST:
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted",
req.env["PATH_INFO"], req.context.get("user"), req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
return func(resource, req, resp, *args, **kwargs)
# Retain username, TODO: Better abstraction with username, e-mail, sn, gn? if config.AUTHORIZATION_BACKEND == "whitelist":
return whitelist_authorize
else:
return member_of(config.ADMINS_GROUP)(func)
return func(self, req, resp, *args, **kwargs)
return wrapped

View File

@ -5,47 +5,64 @@ import re
import socket import socket
import requests import requests
from OpenSSL import crypto from OpenSSL import crypto
from certidude import config, push from certidude import config, push, mailer
from certidude.wrappers import Certificate, Request from certidude.wrappers import Certificate, Request
from certidude.signer import raw_sign from certidude.signer import raw_sign
from certidude import errors from certidude import errors
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
# https://jamielinux.com/docs/openssl-certificate-authority/ # https://jamielinux.com/docs/openssl-certificate-authority/
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
# Cache CA certificate
certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH))
def publish_certificate(func): def publish_certificate(func):
# TODO: Implement e-mail and nginx notifications using hooks # TODO: Implement e-mail and nginx notifications using hooks
def wrapped(csr, *args, **kwargs): def wrapped(csr, *args, **kwargs):
cert = func(csr, *args, **kwargs) cert = func(csr, *args, **kwargs)
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
if cert.email_address:
mailer.send(
"%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address),
"certificate-signed.md",
attachments=(cert,),
certificate=cert)
if config.PUSH_PUBLISH: if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % csr.fingerprint() url = config.PUSH_PUBLISH % csr.fingerprint()
click.echo("Publishing certificate at %s ..." % url) click.echo("Publishing certificate at %s ..." % url)
requests.post(url, data=cert.dump(), requests.post(url, data=cert.dump(),
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
# For deleting request in the web view, use pubkey modulo
push.publish("request-signed", csr.common_name) push.publish("request-signed", csr.common_name)
return cert return cert
return wrapped return wrapped
def get_request(common_name): def get_request(common_name):
if not re.match(RE_HOSTNAME, common_name): if not re.match(RE_HOSTNAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name %s" % repr(common_name))
return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem"))) return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem")))
def get_signed(common_name): def get_signed(common_name):
if not re.match(RE_HOSTNAME, common_name): if not re.match(RE_HOSTNAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name %s" % repr(common_name))
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
def get_revoked(common_name): def get_revoked(common_name):
if not re.match(RE_HOSTNAME, common_name): if not re.match(RE_HOSTNAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name %s" % repr(common_name))
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
def store_request(buf, overwrite=False): def store_request(buf, overwrite=False):
""" """
Store CSR for later processing Store CSR for later processing
@ -92,7 +109,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.common_name)
def list_requests(directory=config.REQUESTS_DIR): def list_requests(directory=config.REQUESTS_DIR):
@ -136,39 +153,50 @@ def delete_request(common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name")
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
request_sha1sum = Request(open(path)).fingerprint() request = Request(open(path))
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.common_name)
# Write empty certificate to long-polling URL # Write empty certificate to long-polling URL
requests.delete(config.PUSH_PUBLISH % request_sha1sum, requests.delete(config.PUSH_PUBLISH % request.common_name,
headers={"User-Agent": "Certidude API"}) headers={"User-Agent": "Certidude API"})
def generate_p12_bundle(common_name):
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
"""
Generate private key, sign certificate and return PKCS#12 bundle
"""
# Construct private key # Construct private key
click.echo("Generating 4096-bit RSA key...") click.echo("Generating %d-bit RSA key..." % key_size)
key = crypto.PKey() key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 512) key.generate_key(crypto.TYPE_RSA, key_size)
# Construct CSR # Construct CSR
csr = crypto.X509Req() csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3 csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key) csr.set_pubkey(key)
csr.get_subject().CN = common_name csr.get_subject().CN = common_name
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr).decode("utf-8") if owner:
if owner.given_name:
csr.get_subject().GN = owner.given_name
if owner.surname:
csr.get_subject().SN = owner.surname
csr.add_extensions([
crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)])
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
# Sign CSR # Sign CSR
cert = sign(Request(buf), overwrite=True) cert = sign(Request(buf), overwrite=True)
# Generate P12 # Generate P12
ca_certs = crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()),
p12 = crypto.PKCS12() p12 = crypto.PKCS12()
p12.set_privatekey( key ) p12.set_privatekey( key )
p12.set_certificate( cert._obj ) p12.set_certificate( cert._obj )
p12.set_ca_certificates( ca_certs ) p12.set_ca_certificates([certificate._obj])
return p12.export() return p12.export(), cert
@publish_certificate @publish_certificate
@ -187,7 +215,7 @@ def sign(req, overwrite=False, delete=True):
elif req.pubkey == old_cert.pubkey: elif req.pubkey == old_cert.pubkey:
return old_cert return old_cert
else: else:
raise FileExistsError("Will not overwrite existing certificate") raise EnvironmentError("Will not overwrite existing certificate")
# Sign via signer process # Sign via signer process
cert_buf = signer_exec("sign-request", req.dump()) cert_buf = signer_exec("sign-request", req.dump())
@ -216,9 +244,9 @@ def sign2(request, overwrite=False, delete=True, lifetime=None):
path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem") path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem")
if os.path.exists(path): if os.path.exists(path):
if overwrite: if overwrite:
revoke(request.common_name) revoke_certificate(request.common_name)
else: else:
raise FileExistsError("File %s already exists!" % path) raise EnvironmentError("File %s already exists!" % path)
buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
with open(path + ".part", "wb") as fh: with open(path + ".part", "wb") as fh:

View File

@ -3,7 +3,6 @@
import asyncore import asyncore
import click import click
import configparser
import hashlib import hashlib
import logging import logging
import os import os
@ -14,11 +13,11 @@ import signal
import socket import socket
import subprocess import subprocess
import sys import sys
from certidude.signer import SignServer from configparser import ConfigParser
from certidude.common import expand_paths from certidude import constants
from certidude.common import expand_paths, ip_address, ip_network
from datetime import datetime from datetime import datetime
from humanize import naturaltime from humanize import naturaltime
from ipaddress import ip_network, ip_address
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from time import sleep from time import sleep
from setproctitle import setproctitle from setproctitle import setproctitle
@ -66,7 +65,7 @@ if os.getuid() >= 1000:
def certidude_request_spawn(fork): def certidude_request_spawn(fork):
from certidude.helpers import certidude_request_certificate from certidude.helpers import certidude_request_certificate
clients = configparser.ConfigParser() clients = ConfigParser()
clients.readfp(open("/etc/certidude/client.conf")) clients.readfp(open("/etc/certidude/client.conf"))
services = ConfigParser() services = ConfigParser()
@ -92,7 +91,7 @@ def certidude_request_spawn(fork):
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
click.echo("Terminated process %d" % pid) click.echo("Terminated process %d" % pid)
os.unlink(pid_path) os.unlink(pid_path)
except (ValueError, ProcessLookupError, FileNotFoundError): except EnvironmentError:
pass pass
if fork: if fork:
@ -137,7 +136,7 @@ def certidude_request_spawn(fork):
# Set up IPsec via NetworkManager # Set up IPsec via NetworkManager
if services.get(endpoint, "service") == "network-manager/strongswan": if services.get(endpoint, "service") == "network-manager/strongswan":
config = configparser.ConfigParser() config = ConfigParser()
config.add_section("connection") config.add_section("connection")
config.add_section("vpn") config.add_section("vpn")
config.add_section("ipv4") config.add_section("ipv4")
@ -218,6 +217,7 @@ def certidude_signer_spawn(kill, no_interaction):
""" """
Spawn privilege isolated signer process Spawn privilege isolated signer process
""" """
from certidude.signer import SignServer
from certidude import config from certidude import config
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
@ -254,7 +254,7 @@ def certidude_signer_spawn(kill, no_interaction):
pid = int(fh.readline()) pid = int(fh.readline())
os.kill(pid, 0) os.kill(pid, 0)
click.echo("Found process with PID %d" % pid) click.echo("Found process with PID %d" % pid)
except (ValueError, ProcessLookupError, FileNotFoundError): except EnvironmentError:
pid = 0 pid = 0
if pid > 0: if pid > 0:
@ -265,7 +265,7 @@ def certidude_signer_spawn(kill, no_interaction):
sleep(1) sleep(1)
os.kill(pid, signal.SIGKILL) os.kill(pid, signal.SIGKILL)
sleep(1) sleep(1)
except ProcessLookupError: except EnvironmentError:
pass pass
child_pid = os.fork() child_pid = os.fork()
@ -280,15 +280,7 @@ def certidude_signer_spawn(kill, no_interaction):
logging.basicConfig( logging.basicConfig(
filename="/var/log/signer.log", filename="/var/log/signer.log",
level=logging.INFO) level=logging.INFO)
server = SignServer( server = SignServer()
config.SIGNER_SOCKET_PATH,
config.AUTHORITY_PRIVATE_KEY_PATH,
config.AUTHORITY_CERTIFICATE_PATH,
config.CERTIFICATE_LIFETIME,
config.CERTIFICATE_BASIC_CONSTRAINTS,
config.CERTIFICATE_KEY_USAGE_FLAGS,
config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
config.REVOCATION_LIST_LIFETIME)
asyncore.loop() asyncore.loop()
@ -363,8 +355,8 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
common_name, common_name,
org_unit, org_unit,
email_address, email_address,
key_usage="nonRepudiation,digitalSignature,keyEncipherment", key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,ikeIntermediate", extended_key_usage="serverAuth",
wait=True) wait=True)
if not os.path.exists(dhparam_path): if not os.path.exists(dhparam_path):
@ -375,7 +367,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
return retval return retval
# TODO: Add dhparam # TODO: Add dhparam
config.write(env.get_template("openvpn-site-to-client.ovpn").render(locals())) config.write(env.get_template("openvpn-site-to-client.ovpn").render(vars()))
click.echo("Generated %s" % config.name) click.echo("Generated %s" % config.name)
click.echo() click.echo()
@ -385,6 +377,74 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
click.echo() click.echo()
@click.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("url")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--site-config", "-o",
default="/etc/nginx/sites-available/%s.conf" % HOSTNAME,
type=click.File(mode="w", atomic=True, lazy=True),
help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % HOSTNAME)
@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
@expand_paths()
def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client):
# TODO: Intelligent way of getting last IP address in the subnet
from certidude.helpers import certidude_request_certificate
if not os.path.exists(certificate_path):
click.echo("As HTTPS server certificate needs specific key usage extensions please")
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
click.echo()
retval = certidude_request_certificate(url, key_path, request_path,
certificate_path, authority_path, common_name, org_unit,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
dns = constants.FQDN, wait=True, bundle=True)
if not os.path.exists(dhparam_path):
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
subprocess.check_call(cmd)
if retval:
return retval
context = globals() # Grab constants.BLAH
context.update(locals())
if os.path.exists(site_config.name):
click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
else:
site_config.write(env.get_template("nginx-https-site.conf").render(context))
click.echo("Generated %s" % site_config.name)
if os.path.exists(tls_config.name):
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
else:
tls_config.write(env.get_template("nginx-tls.conf").render(context))
click.echo("Generated %s" % tls_config.name)
click.echo()
click.echo("Inspect configuration files, enable it and start nginx service:")
click.echo()
click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % (
os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"),
os.path.basename(site_config.name)))
click.secho(" service nginx restart", bold=True)
click.echo()
@click.command("client", help="Set up OpenVPN client") @click.command("client", help="Set up OpenVPN client")
@click.argument("url") @click.argument("url")
@click.argument("remote") @click.argument("remote")
@ -419,7 +479,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
return retval return retval
# TODO: Add dhparam # TODO: Add dhparam
config.write(env.get_template("openvpn-client-to-site.ovpn").render(locals())) config.write(env.get_template("openvpn-client-to-site.ovpn").render(vars()))
click.echo("Generated %s" % config.name) click.echo("Generated %s" % config.name)
click.echo() click.echo()
@ -435,8 +495,8 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@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)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") @click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", default=None, type=ip_address, help="IP address associated with the certificate, none by default") @click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
@click.option("--config", "-o", @click.option("--config", "-o",
default="/etc/ipsec.conf", default="/etc/ipsec.conf",
@ -473,7 +533,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
common_name, common_name,
org_unit, org_unit,
email_address, email_address,
key_usage="nonRepudiation,digitalSignature,keyEncipherment", key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
ip_address=local, ip_address=local,
dns=fqdn, dns=fqdn,
@ -482,7 +542,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
if retval: if retval:
return retval return retval
config.write(env.get_template("strongswan-site-to-client.conf").render(locals())) config.write(env.get_template("strongswan-site-to-client.conf").render(vars()))
secrets.write(": RSA %s\n" % key_path) secrets.write(": RSA %s\n" % key_path)
click.echo("Generated %s and %s" % (config.name, secrets.name)) click.echo("Generated %s and %s" % (config.name, secrets.name))
@ -539,7 +599,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
return retval return retval
# TODO: Add dhparam # TODO: Add dhparam
config.write(env.get_template("strongswan-client-to-site.conf").render(locals())) config.write(env.get_template("strongswan-client-to-site.conf").render(vars()))
secrets.write(": RSA %s\n" % key_path) secrets.write(": RSA %s\n" % key_path)
click.echo("Generated %s and %s" % (config.name, secrets.name)) click.echo("Generated %s and %s" % (config.name, secrets.name))
@ -584,7 +644,7 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o
csum = csummer.hexdigest() csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
config = configparser.ConfigParser() config = ConfigParser()
config.add_section("connection") config.add_section("connection")
config.add_section("vpn") config.add_section("vpn")
config.add_section("ipv4") config.add_section("ipv4")
@ -620,11 +680,12 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o
subprocess.call(("nmcli", "c", "up", "uuid", uuid)) subprocess.call(("nmcli", "c", "up", "uuid", uuid))
@click.command("production", help="Set up nginx and uwsgi") @click.command("production", help="Set up nginx, uwsgi and cron")
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME) @click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files") @click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab") @click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab")
@click.option("--push-server", default=None, help="Push server URL")
@click.option("--nginx-config", "-n", @click.option("--nginx-config", "-n",
default="/etc/nginx/nginx.conf", default="/etc/nginx/nginx.conf",
type=click.File(mode="w", atomic=True, lazy=True), type=click.File(mode="w", atomic=True, lazy=True),
@ -642,19 +703,36 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
subprocess.check_call(cmd) subprocess.check_call(cmd)
if subprocess.call("net ads testjoin", shell=True): if subprocess.call("net ads testjoin", shell=True):
click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", stderr=True) click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", err=True)
exit(255) exit(255)
if not os.path.exists(kerberos_keytab): if not os.path.exists(kerberos_keytab):
subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P") subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P")
click.echo("Created Kerberos keytab in '%s'" % kerberos_keytab) click.echo("Created service principal in Kerberos keytab '%s'" % kerberos_keytab)
if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"):
# Fetch Kerberos ticket for system account
cp = ConfigParser()
cp.read("/etc/samba/smb.conf")
domain = cp.get("global", "realm").lower()
base = ",".join(["dc=" + j for j in domain.split(".")])
with open("/etc/cron.hourly/certidude", "w") as fh:
fh.write("#!/bin/bash\n")
fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name"))
fh.write("chown certidude /run/certidude/krb5cc-new\n")
fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n")
os.chmod("/etc/cron.hourly/certidude", 0o755)
click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal")
else:
click.echo("Warning: cronjob for Kerberos ticket renewal not created, LDAP with GSSAPI will not be available!")
if not static_path.endswith("/"): if not static_path.endswith("/"):
static_path += "/" static_path += "/"
nginx_config.write(env.get_template("nginx.conf").render(locals())) nginx_config.write(env.get_template("nginx.conf").render(vars()))
click.echo("Generated: %s" % nginx_config.name) click.echo("Generated: %s" % nginx_config.name)
uwsgi_config.write(env.get_template("uwsgi.ini").render(locals())) uwsgi_config.write(env.get_template("uwsgi.ini").render(vars()))
click.echo("Generated: %s" % uwsgi_config.name) click.echo("Generated: %s" % uwsgi_config.name)
if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"):
@ -663,7 +741,7 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name) click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name)
if not push_server: if not push_server:
click.echo("Remember to install nginx with wandenberg/nginx-push-stream-module!") click.echo("Remember to install nchan instead of regular nginx!")
@click.command("authority", help="Set up Certificate Authority in a directory") @click.command("authority", help="Set up Certificate Authority in a directory")
@ -735,6 +813,9 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60)
ca.set_issuer(ca.get_subject()) ca.set_issuer(ca.get_subject())
ca.set_pubkey(key) ca.set_pubkey(key)
# add_extensions shall be called only once and
# there has to be only one subjectAltName!
ca.add_extensions([ ca.add_extensions([
crypto.X509Extension( crypto.X509Extension(
b"basicConstraints", b"basicConstraints",
@ -746,7 +827,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
b"keyCertSign, cRLSign"), b"keyCertSign, cRLSign"),
crypto.X509Extension( crypto.X509Extension(
b"extendedKeyUsage", b"extendedKeyUsage",
True, False,
b"serverAuth,1.3.6.1.5.5.8.2.2"), b"serverAuth,1.3.6.1.5.5.8.2.2"),
crypto.X509Extension( crypto.X509Extension(
b"subjectKeyIdentifier", b"subjectKeyIdentifier",
@ -756,21 +837,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
crypto.X509Extension( crypto.X509Extension(
b"crlDistributionPoints", b"crlDistributionPoints",
False, False,
crl_distribution_points.encode("ascii")) crl_distribution_points.encode("ascii")),
])
subject_alt_name = "email:%s" % email_address
ca.add_extensions([
crypto.X509Extension( crypto.X509Extension(
b"subjectAltName", b"subjectAltName",
False, False,
subject_alt_name.encode("ascii")) "DNS: %s, email: %s" % (common_name.encode("ascii"), email_address.encode("ascii")))
])
ca.add_extensions([
crypto.X509Extension(
b"subjectAltName",
True,
("DNS:%s" % common_name).encode("ascii"))
]) ])
if ocsp_responder_url: if ocsp_responder_url:
@ -819,7 +890,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
# Set permission bits to 640 # Set permission bits to 640
os.umask(0o137) os.umask(0o137)
with open(certidude_conf, "w") as fh: with open(certidude_conf, "w") as fh:
fh.write(env.get_template("certidude.conf").render(locals())) fh.write(env.get_template("certidude.conf").render(vars()))
with open(ca_crt, "wb") as fh: with open(ca_crt, "wb") as fh:
fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca))
@ -988,12 +1059,23 @@ def certidude_sign(common_name, overwrite, lifetime):
click.echo("Added extension %s: %s" % (key, value)) click.echo("Added extension %s: %s" % (key, value))
click.echo() click.echo()
@click.command("serve", help="Run built-in HTTP server") @click.command("serve", help="Run built-in HTTP server")
@click.option("-u", "--user", default="certidude", help="Run as user") @click.option("-u", "--user", default="certidude", help="Run as user")
@click.option("-p", "--port", default=80, help="Listen port") @click.option("-p", "--port", default=80, help="Listen port")
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address") @click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
@click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA") @click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA")
def certidude_serve(user, port, listen, enable_signature): def certidude_serve(user, port, listen, enable_signature):
from certidude import config
click.echo("Users subnets: %s" %
", ".join([str(j) for j in config.USER_SUBNETS]))
click.echo("Administrative subnets: %s" %
", ".join([str(j) for j in config.ADMIN_SUBNETS]))
click.echo("Auto-sign enabled for following subnets: %s" %
", ".join([str(j) for j in config.AUTOSIGN_SUBNETS]))
click.echo("Request submissions allowed from following subnets: %s" %
", ".join([str(j) for j in config.REQUEST_SUBNETS]))
logging.basicConfig( logging.basicConfig(
filename='/var/log/certidude.log', filename='/var/log/certidude.log',
@ -1004,13 +1086,15 @@ def certidude_serve(user, port, listen, enable_signature):
from wsgiref.simple_server import make_server, WSGIServer from wsgiref.simple_server import make_server, WSGIServer
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from certidude.api import certidude_app, StaticResource from certidude.api import certidude_app, StaticResource
from certidude import config
class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
pass pass
click.echo("Listening on %s:%d" % (listen, port)) click.echo("Listening on %s:%d" % (listen, port))
# TODO: Bind before dropping privileges,
# but create app (sqlite log files!) after dropping privileges
app = certidude_app() app = certidude_app()
app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static"))) app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static")))
@ -1023,25 +1107,25 @@ def certidude_serve(user, port, listen, enable_signature):
from jinja2.debug import make_traceback as _make_traceback from jinja2.debug import make_traceback as _make_traceback
"".encode("charmap") "".encode("charmap")
if config.AUTHENTICATION_BACKEND == "pam": restricted_groups = []
if config.AUTHENTICATION_BACKENDS == {"pam"}:
# PAM needs access to /etc/shadow # PAM needs access to /etc/shadow
import grp import grp
name, passwd, gid, mem = grp.getgrnam("shadow") name, passwd, gid, mem = grp.getgrnam("shadow")
click.echo("Adding current user to shadow group due to PAM authentication backend") click.echo("Adding current user to shadow group due to PAM authentication backend")
os.setgroups([gid]) restricted_groups.append(gid)
else:
os.setgroups([])
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user)
if uid == 0: restricted_groups.append(gid)
click.echo("Please specify unprivileged user")
exit(254)
os.setgroups(restricted_groups)
os.setgid(gid) os.setgid(gid)
os.setuid(uid) os.setuid(uid)
click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" %
(user, uid, gid, ", ".join([str(j) for j in os.getgroups()]))) (user, os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()])))
os.umask(0o007) os.umask(0o007)
elif os.getuid() == 0: elif os.getuid() == 0:
@ -1076,6 +1160,7 @@ certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_strongswan)
certidude_setup.add_command(certidude_setup_client) certidude_setup.add_command(certidude_setup_client)
certidude_setup.add_command(certidude_setup_production) certidude_setup.add_command(certidude_setup_production)
certidude_setup.add_command(certidude_setup_nginx)
certidude_request.add_command(certidude_request_spawn) certidude_request.add_command(certidude_request_spawn)
certidude_signer.add_command(certidude_signer_spawn) certidude_signer.add_command(certidude_signer_spawn)
entry_point.add_command(certidude_setup) entry_point.add_command(certidude_setup)

View File

@ -1,6 +1,13 @@
import os import os
import click import click
import ipaddress
def ip_network(j):
return ipaddress.ip_network(unicode(j))
def ip_address(j):
return ipaddress.ip_address(unicode(j))
def expand_paths(): def expand_paths():
""" """

View File

@ -4,50 +4,52 @@ import codecs
import configparser import configparser
import ipaddress import ipaddress
import os import os
import socket
import string import string
from random import choice from random import choice
from urllib.parse import urlparse from urllib.parse import urlparse
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
cp = configparser.ConfigParser() cp = configparser.ConfigParser()
cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8"))
AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam AUTHENTICATION_BACKENDS = set([j for j in
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix
ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j]) USER_SUBNETS = set([ipaddress.ip_network(j) for j in
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j]) cp.get("authorization", "user subnets").split(" ") if j])
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign_subnets").split(" ") if j]) ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "request_subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS)
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "autosign subnets").split(" ") if j])
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
SIGNER_PID_PATH = "/run/certidude/signer.pid" SIGNER_PID_PATH = "/run/certidude/signer.pid"
AUTHORITY_DIR = "/var/lib/certidude" AUTHORITY_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private_key_path") AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate_path") AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
REQUESTS_DIR = cp.get("authority", "requests_dir") REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed_dir") SIGNED_DIR = cp.get("authority", "signed dir")
REVOKED_DIR = cp.get("authority", "revoked_dir") REVOKED_DIR = cp.get("authority", "revoked dir")
OUTBOX = cp.get("authority", "outbox")
#LOG_DATA = cp.get("logging", "database")
CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE"
CERTIFICATE_KEY_USAGE_FLAGS = "nonRepudiation,digitalSignature,keyEncipherment" CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment"
CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth" CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth"
CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate_lifetime")) CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate lifetime"))
REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation_list_lifetime")) REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation list lifetime"))
PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)]) PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)])
PUSH_TOKEN = "ca" PUSH_TOKEN = "ca"
try: try:
PUSH_EVENT_SOURCE = cp.get("push", "event_source") PUSH_EVENT_SOURCE = cp.get("push", "event source")
PUSH_LONG_POLL = cp.get("push", "long_poll") PUSH_LONG_POLL = cp.get("push", "long poll")
PUSH_PUBLISH = cp.get("push", "publish") PUSH_PUBLISH = cp.get("push", "publish")
except configparser.NoOptionError: except configparser.NoOptionError:
PUSH_SERVER = cp.get("push", "server") or "http://localhost" PUSH_SERVER = cp.get("push", "server") or "http://localhost"
@ -55,18 +57,41 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
o = urlparse(cp.get("authority", "database") if cp.has_option("authority", "database") else "")
if not o.scheme: TAGGING_BACKEND = cp.get("tagging", "backend")
DATABASE_POOL = None LOGGING_BACKEND = cp.get("logging", "backend")
elif o.scheme == "mysql": LEASES_BACKEND = cp.get("leases", "backend")
import mysql.connector
DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool(
pool_size = 32, if "whitelist" == AUTHORIZATION_BACKEND:
user=o.username, USERS_WHITELIST = set([j for j in cp.get("authorization", "users whitelist").split(" ") if j])
password=o.password, ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
host=o.hostname, elif "posix" == AUTHORIZATION_BACKEND:
database=o.path[1:]) USERS_GROUP = cp.get("authorization", "posix user group")
ADMINS_GROUP = cp.get("authorization", "posix admin group")
elif "ldap" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "ldap user group")
ADMINS_GROUP = cp.get("authorization", "ldap admin group")
else: else:
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database is supported" % o.scheme) raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter")
LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter")
LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter")
for line in open("/etc/ldap/ldap.conf"):
line = line.strip().lower()
if "#" in line:
line, _ = line.split("#", 1)
if not " " in line:
continue
key, value = line.split(" ", 1)
if key == "uri":
LDAP_SERVERS = set([j for j in value.split(" ") if j])
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
elif key == "base":
LDAP_BASE = value
else:
click.echo("No LDAP servers specified in /etc/ldap/ldap.conf")

12
certidude/constants.py Normal file
View File

@ -0,0 +1,12 @@
import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
if "." in FQDN:
HOSTNAME, DOMAIN = FQDN.split(".", 1)
else:
HOSTNAME, DOMAIN = FQDN, "local"
click.echo("Unable to determine domain of this computer, falling back to local")
EXTENSION_WHITELIST = set(["subjectAltName"])

View File

@ -1,13 +1,39 @@
# encoding: utf-8
import falcon import falcon
import ipaddress import ipaddress
import json import json
import logging
import re import re
import types import types
from datetime import date, time, datetime from datetime import date, time, datetime
from OpenSSL import crypto from OpenSSL import crypto
from certidude.wrappers import Request, Certificate from certidude.wrappers import Request, Certificate
from urllib.parse import urlparse
logger = logging.getLogger("api")
def csrf_protection(func):
"""
Protect resource from common CSRF attacks by checking user agent and referrer
"""
def wrapped(self, req, resp, *args, **kwargs):
# Assume curl and python-requests are used intentionally
if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"):
return func(self, req, resp, *args, **kwargs)
# For everything else assert referrer
referrer = req.headers.get("REFERER")
if referrer:
scheme, netloc, path, params, query, fragment = urlparse(referrer)
if netloc == req.host:
return func(self, req, resp, *args, **kwargs)
# Kaboom!
logger.warning("Prevented clickbait from '%s' with user agent '%s'",
referrer or "-", req.user_agent)
raise falcon.HTTPUnauthorized("Forbidden",
"No suitable UA or referrer provided, cross-site scripting disabled")
return wrapped
def event_source(func): def event_source(func):
def wrapped(self, req, resp, *args, **kwargs): def wrapped(self, req, resp, *args, **kwargs):
@ -15,7 +41,6 @@ def event_source(func):
resp.status = falcon.HTTP_SEE_OTHER resp.status = falcon.HTTP_SEE_OTHER
resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid
resp.body = "Redirecting to:" + resp.location resp.body = "Redirecting to:" + resp.location
print("Delegating EventSource handling to:", resp.location)
return func(self, req, resp, *args, **kwargs) return func(self, req, resp, *args, **kwargs)
return wrapped return wrapped
@ -24,9 +49,10 @@ class MyEncoder(json.JSONEncoder):
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \ "organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage" "key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage"
CERTIFICATE_ATTRIBUTES = "revokable", "identity", "changed", "common_name", \ CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \ "organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"key_type", "key_length", "sha256sum", "serial_number", "key_usage" "key_type", "key_length", "sha256sum", "serial_number", "key_usage", \
"signed", "expires"
def default(self, obj): def default(self, obj):
if isinstance(obj, crypto.X509Name): if isinstance(obj, crypto.X509Name):
@ -60,18 +86,25 @@ def serialize(func):
Falcon response serialization Falcon response serialization
""" """
def wrapped(instance, req, resp, **kwargs): def wrapped(instance, req, resp, **kwargs):
assert not req.get_param("unicode") or req.get_param("unicode") == u"", "Unicode sanity check failed" resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); 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 resp.body is None: if resp.body is None:
if req.get_header("Accept").split(",")[0] == "application/json": if req.accept.startswith("application/json"):
resp.set_header("Content-Type", "application/json") resp.set_header("Content-Type", "application/json")
resp.set_header("Content-Disposition", "inline") resp.set_header("Content-Disposition", "inline")
resp.body = json.dumps(r, cls=MyEncoder) resp.body = json.dumps(r, cls=MyEncoder)
elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
resp.set_header("Content-Type", r.content_type)
resp.set_header("Content-Disposition",
("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
resp.body = r.dump()
else: else:
resp.body = repr(r) logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept))
raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json or %s" % r.content_type)
return r return r
return wrapped return wrapped

38
certidude/firewall.py Normal file
View File

@ -0,0 +1,38 @@
import falcon
import logging
logger = logging.getLogger("api")
def whitelist_subnets(subnets):
"""
Validate source IP address of API call against subnet list
"""
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
# Check for administration subnet whitelist
for subnet in subnets:
if req.context.get("remote_addr") in subnet:
break
else:
logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted",
req.env["PATH_INFO"],
req.context.get("user", "unauthenticated user"),
req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr)
return func(self, req, resp, *args, **kwargs)
return wrapped
return wrapper
def whitelist_content_types(*content_types):
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
for content_type in content_types:
if req.get_header("Content-Type") == content_type:
return func(self, req, resp, *args, **kwargs)
raise falcon.HTTPUnsupportedMediaType(
"This API call accepts only %s content type" % ", ".join(content_types))
return wrapped
return wrapper

View File

@ -6,7 +6,7 @@ from certidude import errors
from certidude.wrappers import Certificate, Request 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=None, 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=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
""" """
Exchange CSR for certificate using Certidude HTTP API server Exchange CSR for certificate using Certidude HTTP API server
""" """
@ -41,7 +41,8 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
click.echo("Attempting to fetch CA certificate from %s" % authority_url) click.echo("Attempting to fetch CA certificate from %s" % authority_url)
try: try:
r = requests.get(authority_url) r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
except crypto.Error: except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % r.text) raise ValueError("Failed to parse PEM: %s" % r.text)
@ -53,7 +54,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
try: try:
request = Request(open(request_path)) request = Request(open(request_path))
click.echo("Found signing request: %s" % request_path) click.echo("Found signing request: %s" % request_path)
except FileNotFoundError: except EnvironmentError:
# Construct private key # Construct private key
click.echo("Generating 4096-bit RSA key...") click.echo("Generating 4096-bit RSA key...")
@ -69,10 +70,11 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
csr = crypto.X509Req() csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3 csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key) csr.set_pubkey(key)
csr.get_subject().CN = common_name
request = Request(csr) request = Request(csr)
# Set subject attributes # Set subject attributes
request.common_name = common_name
if given_name: if given_name:
request.given_name = given_name request.given_name = given_name
if surname: if surname:
@ -83,20 +85,20 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
# Collect subject alternative names # Collect subject alternative names
subject_alt_name = set() subject_alt_name = set()
if email_address: if email_address:
subject_alt_name.add("email:" + email_address) subject_alt_name.add("email:%s" % email_address)
if ip_address: if ip_address:
subject_alt_name.add("IP:" + ip_address) subject_alt_name.add("IP:%s" % ip_address)
if dns: if dns:
subject_alt_name.add("DNS:" + dns) subject_alt_name.add("DNS:%s" % dns)
# Set extensions # Set extensions
extensions = [] extensions = []
if key_usage: if key_usage:
extensions.append(("keyUsage", key_usage, True)) extensions.append(("keyUsage", key_usage, True))
if extended_key_usage: if extended_key_usage:
extensions.append(("extendedKeyUsage", extended_key_usage, True)) extensions.append(("extendedKeyUsage", extended_key_usage, False))
if subject_alt_name: if subject_alt_name:
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True)) extensions.append(("subjectAltName", ", ".join(subject_alt_name), False))
request.set_extensions(extensions) request.set_extensions(extensions)
# Dump CSR # Dump CSR
@ -113,7 +115,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
click.echo("Submitting to %s, waiting for response..." % request_url) click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url, submission = requests.post(request_url,
data=open(request_path), data=open(request_path),
headers={"User-Agent": "Certidude", "Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert"}) headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"})
if submission.status_code == requests.codes.ok: if submission.status_code == requests.codes.ok:
pass pass
@ -131,12 +133,18 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
try: try:
cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
except crypto.Error: except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % buf) raise ValueError("Failed to parse PEM: %s" % submission.text)
os.umask(0o022) os.umask(0o022)
with open(certificate_path + ".part", "w") as fh: with open(certificate_path + ".part", "w") as fh:
# Dump certificate
fh.write(submission.text) fh.write(submission.text)
# Bundle CA certificate, necessary for nginx
if bundle:
with open(authority_path) as ch:
fh.write(ch.read())
click.echo("Writing certificate to: %s" % certificate_path) click.echo("Writing certificate to: %s" % certificate_path)
os.rename(certificate_path + ".part", certificate_path) os.rename(certificate_path + ".part", certificate_path)

View File

@ -1,104 +1,90 @@
import os import os
import smtplib import smtplib
from time import sleep from markdown import markdown
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from urllib.parse import urlparse from urllib.parse import urlparse
class Mailer(object): env = Environment(loader=PackageLoader("certidude", "templates/mail"))
def __init__(self, url):
scheme, netloc, path, params, query, fragment = urlparse(url)
scheme = scheme.lower()
if path: def send(recipients, template, attachments=(), **context):
raise ValueError("Path for URL not supported") from certidude import authority, config
if params: if not config.OUTBOX:
raise ValueError("Parameters for URL not supported") # Mailbox disabled, don't send e-mail
if query: return
raise ValueError("Query for URL not supported")
if fragment: if not recipients:
raise ValueError("Fragment for URL not supported") raise ValueError("No e-mail recipients specified!")
scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
scheme = scheme.lower()
if path:
raise ValueError("Path for URL not supported")
if params:
raise ValueError("Parameters for URL not supported")
if query:
raise ValueError("Query for URL not supported")
if fragment:
raise ValueError("Fragment for URL not supported")
self.username = None username = None
self.password = "" password = ""
if scheme == "smtp": if scheme == "smtp":
self.secure = False secure = False
self.port = 25 port = 25
elif scheme == "smtps": elif scheme == "smtps":
self.secure = True secure = True
self.port = 465 port = 465
else:
raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme)
if "@" in netloc:
credentials, netloc = netloc.split("@")
if ":" in credentials:
username, password = credentials.split(":")
else: else:
raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme) username = credentials
if "@" in netloc: if ":" in netloc:
credentials, netloc = netloc.split("@") server, port_str = netloc.split(":")
port = int(port_str)
if ":" in credentials: else:
self.username, self.password = credentials.split(":") server = netloc
else:
self.username = credentials
if ":" in netloc:
self.server, port_str = netloc.split(":")
self.port = int(port_str)
else:
self.server = netloc
self.env = Environment(loader=PackageLoader("certidude", "email_templates"))
self.conn = None
def reconnect(self):
# Gmail employs some sort of IPS
# https://accounts.google.com/DisplayUnlockCaptcha
print("Connecting to:", self.server, self.port)
self.conn = smtplib.SMTP(self.server, self.port)
if self.secure:
self.conn.starttls()
if self.username and self.password:
self.conn.login(self.username, self.password)
def enqueue(self, sender, recipients, subject, template, **context):
self.send(sender, recipients, subject, template, **context)
def send(self, sender, recipients, subject, template, **context): subject, text = env.get_template(template).render(context).split("\n\n", 1)
html = markdown(text)
recipients = [j for j in recipients if j] msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = authority.certificate.email_address
msg["To"] = recipients
if not recipients: part1 = MIMEText(text, "plain")
print("No recipients to send e-mail to!") part2 = MIMEText(html, "html")
return
print("Sending e-mail to:", recipients, "body follows:")
msg = MIMEMultipart("alternative") msg.attach(part1)
msg["Subject"] = subject msg.attach(part2)
msg["From"] = sender
msg["To"] = ", ".join(recipients)
text = self.env.get_template(template + ".txt").render(context) for attachment in attachments:
html = self.env.get_template(template + ".html").render(context) part = MIMEBase(*attachment.content_type.split("/"))
part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename)
part.set_payload(attachment.dump())
msg.attach(part)
print(text) # Gmail employs some sort of IPS
# https://accounts.google.com/DisplayUnlockCaptcha
conn = smtplib.SMTP(server, port)
if secure:
conn.starttls()
if username and password:
conn.login(username, password)
part1 = MIMEText(text, "plain") conn.sendmail(authority.certificate.email_address, recipients, msg.as_string())
part2 = MIMEText(html, "html")
msg.attach(part1)
msg.attach(part2)
backoff = 1
while True:
try:
if not self.conn:
self.reconnect()
self.conn.sendmail(sender, recipients, msg.as_string())
return
except smtplib.SMTPServerDisconnected:
print("Connection to %s unexpectedly closed, probably TCP timeout, backing off for %d second" % (self.server, backoff))
self.reconnect()
backoff = backoff * 2
sleep(backoff)

View File

@ -1,34 +1,17 @@
import logging import logging
import time import time
from certidude.api.tag import RelationalMixin
class MySQLLogHandler(logging.Handler): class LogHandler(logging.Handler, RelationalMixin):
SQL_CREATE_TABLES = "log_tables.sql"
SQL_CREATE_TABLE = """CREATE TABLE IF NOT EXISTS log( def __init__(self, uri):
created datetime, facility varchar(30), level int,
severity varchar(10), message text, module varchar(20),
func varchar(20), lineno int, exception text, process int,
thread text, thread_name text)"""
SQL_INSERT_ENTRY = """insert into log( created, facility, level, severity,
message, module, func, lineno, exception, process, thread,
thread_name) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
def __init__(self, pool):
logging.Handler.__init__(self) logging.Handler.__init__(self)
self.pool = pool RelationalMixin.__init__(self, uri)
conn = self.pool.get_connection()
cur = conn.cursor()
cur.execute(self.SQL_CREATE_TABLE)
conn.commit()
cur.close()
conn.close()
def emit(self, record): def emit(self, record):
conn = self.pool.get_connection() self.sql_execute("log_insert_entry.sql",
cur = conn.cursor()
cur.execute(self.SQL_INSERT_ENTRY, (
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)), time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)),
record.name, record.name,
record.levelno, record.levelno,
@ -39,7 +22,4 @@ class MySQLLogHandler(logging.Handler):
logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "", logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "",
record.process, record.process,
record.thread, record.thread,
record.threadName)) record.threadName)
conn.commit()
cur.close()
conn.close()

View File

@ -1,7 +1,9 @@
import click import click
import json import json
import logging
import requests import requests
from datetime import datetime
from certidude import config from certidude import config
@ -9,13 +11,29 @@ def publish(event_type, event_data):
""" """
Publish event on push server Publish event on push server
""" """
if not isinstance(event_data, str): if not isinstance(event_data, basestring):
from certidude.decorators import MyEncoder from certidude.decorators import MyEncoder
event_data = json.dumps(event_data, cls=MyEncoder) event_data = json.dumps(event_data, cls=MyEncoder)
notification = requests.post( url = config.PUSH_PUBLISH % config.PUSH_TOKEN
config.PUSH_PUBLISH % config.PUSH_TOKEN, click.echo("Publishing %s event %s on %s" % (event_type, event_data, url))
data=event_data,
headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"})
try:
notification = requests.post(
url,
data=event_data,
headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"})
except requests.exceptions.ConnectionError:
click.echo("Failed to submit event to push server: %s" % repr(event_data))
class PushLogHandler(logging.Handler):
"""
To be used with Python log handling framework for publishing log entries
"""
def emit(self, record):
from certidude.push import publish
publish("log-entry", dict(
created = datetime.fromtimestamp(record.created),
message = record.msg % record.args,
severity = record.levelname.lower()))

103
certidude/relational.py Normal file
View File

@ -0,0 +1,103 @@
import click
import re
import os
from urllib.parse import urlparse
SCRIPTS = {}
class RelationalMixin(object):
"""
Thin wrapper around SQLite and MySQL database connectors
"""
SQL_CREATE_TABLES = ""
def __init__(self, uri):
self.uri = urlparse(uri)
if self.SQL_CREATE_TABLES and self.SQL_CREATE_TABLES not in SCRIPTS:
conn = self.sql_connect()
cur = conn.cursor()
with open(self.sql_resolve_script(self.SQL_CREATE_TABLES)) as fh:
click.echo("Executing: %s" % fh.name)
if self.uri.scheme == "sqlite":
cur.executescript(fh.read())
else:
cur.execute(fh.read())
conn.commit()
cur.close()
conn.close()
def sql_connect(self):
if self.uri.scheme == "mysql":
import mysql.connector
return mysql.connector.connect(
user=self.uri.username,
password=self.uri.password,
host=self.uri.hostname,
database=self.uri.path[1:])
elif self.uri.scheme == "sqlite":
if self.uri.netloc:
raise ValueError("Malformed database URI %s" % self.uri)
import sqlite3
return sqlite3.connect(self.uri.path)
else:
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme)
def sql_resolve_script(self, filename):
return os.path.realpath(os.path.join(os.path.dirname(__file__),
"sql", self.uri.scheme, filename))
def sql_load(self, filename):
if filename in SCRIPTS:
return SCRIPTS[filename]
fh = open(self.sql_resolve_script(filename))
click.echo("Caching SQL script: %s" % fh.name)
buf = re.sub("\s*\n\s*", " ", fh.read())
SCRIPTS[filename] = buf
fh.close()
return buf
def sql_execute(self, script, *args):
conn = self.sql_connect()
cursor = conn.cursor()
click.echo("Executing %s with %s" % (script, args))
cursor.execute(self.sql_load(script), args)
rowid = cursor.lastrowid
conn.commit()
cursor.close()
conn.close()
return rowid
def iterfetch(self, query, *args):
conn = self.sql_connect()
cursor = conn.cursor()
cursor.execute(query, args)
cols = [j[0] for j in cursor.description]
def g():
for row in cursor:
yield dict(zip(cols, row))
cursor.close()
conn.close()
return tuple(g())
def sql_fetchone(self, query, *args):
conn = self.sql_connect()
cursor = conn.cursor()
cursor.execute(query, args)
cols = [j[0] for j in cursor.description]
for row in cursor:
r = dict(zip(cols, row))
cursor.close()
conn.close()
return r
return None

View File

@ -6,6 +6,7 @@ import socket
import os import os
import asyncore import asyncore
import asynchat import asynchat
from certidude import constants, config
from datetime import datetime from datetime import datetime
from OpenSSL import crypto from OpenSSL import crypto
@ -26,86 +27,83 @@ certificate authoirty (basicConstraints=CA:TRUE) or
TLS server certificates (extendedKeyUsage=serverAuth). TLS server certificates (extendedKeyUsage=serverAuth).
""" """
EXTENSION_WHITELIST = set(["subjectAltName"])
def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None): def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None):
""" """
Sign certificate signing request directly with private key assuming it's readable by the process Sign certificate signing request directly with private key assuming it's readable by the process
""" """
# Initialize X.509 certificate object # Initialize X.509 certificate object
cert = crypto.X509() cert = crypto.X509()
cert.set_version(2) # This corresponds to X.509v3 cert.set_version(2) # This corresponds to X.509v3
# Set public key # Set public key
cert.set_pubkey(request.get_pubkey()) cert.set_pubkey(request.get_pubkey())
# Set issuer # Set issuer
cert.set_issuer(ca_cert.get_subject()) cert.set_issuer(ca_cert.get_subject())
# TODO: Assert openssl.cnf policy for subject attributes # Copy attributes from CA
# if request.get_subject().O != ca_cert.get_subject().O: if ca_cert.get_subject().C:
# raise ValueError("Orgnization name mismatch!") cert.get_subject().C = ca_cert.get_subject().C
# if request.get_subject().C != ca_cert.get_subject().C: if ca_cert.get_subject().ST:
# raise ValueError("Country mismatch!") cert.get_subject().ST = ca_cert.get_subject().ST
if ca_cert.get_subject().L:
cert.get_subject().L = ca_cert.get_subject().L
if ca_cert.get_subject().O:
cert.get_subject().O = ca_cert.get_subject().O
# Copy attributes from CA # Copy attributes from request
if ca_cert.get_subject().C: cert.get_subject().CN = request.get_subject().CN
cert.get_subject().C = ca_cert.get_subject().C
if ca_cert.get_subject().ST:
cert.get_subject().ST = ca_cert.get_subject().ST
if ca_cert.get_subject().L:
cert.get_subject().L = ca_cert.get_subject().L
if ca_cert.get_subject().O:
cert.get_subject().O = ca_cert.get_subject().O
# Copy attributes from request if request.get_subject().SN:
cert.get_subject().CN = request.get_subject().CN cert.get_subject().SN = request.get_subject().SN
req_subject = request.get_subject() if request.get_subject().GN:
if hasattr(req_subject, "OU") and req_subject.OU: cert.get_subject().GN = request.get_subject().GN
cert.get_subject().OU = req_subject.OU
# Copy e-mail, key usage, extended key from request if request.get_subject().OU:
for extension in request.get_extensions(): cert.get_subject().OU = req_subject.OU
cert.add_extensions([extension])
# TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request # Copy e-mail, key usage, extended key from request
for extension in request.get_extensions():
cert.add_extensions([extension])
# Override basic constraints if nececssary # TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request
if basic_constraints:
# Override basic constraints if nececssary
if basic_constraints:
cert.add_extensions([
crypto.X509Extension(
b"basicConstraints",
True,
basic_constraints.encode("ascii"))])
if key_usage:
try:
cert.add_extensions([ cert.add_extensions([
crypto.X509Extension( crypto.X509Extension(
b"basicConstraints", b"keyUsage",
True, True,
basic_constraints.encode("ascii"))]) key_usage.encode("ascii"))])
except crypto.Error:
raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage)
if key_usage: if extended_key_usage:
try: cert.add_extensions([
cert.add_extensions([ crypto.X509Extension(
crypto.X509Extension( b"extendedKeyUsage",
b"keyUsage", True,
True, extended_key_usage.encode("ascii"))])
key_usage.encode("ascii"))])
except crypto.Error:
raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage)
if extended_key_usage: # Set certificate lifetime
cert.add_extensions([ cert.gmtime_adj_notBefore(-3600)
crypto.X509Extension( cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
b"extendedKeyUsage",
True,
extended_key_usage.encode("ascii"))])
# Set certificate lifetime # Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff
cert.gmtime_adj_notBefore(-3600) cert.set_serial_number(random.randint(
cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60) 0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff))
# Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff cert.sign(private_key, 'sha256')
cert.set_serial_number(random.randint( return cert
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff))
cert.sign(private_key, 'sha1')
return cert
class SignHandler(asynchat.async_chat): class SignHandler(asynchat.async_chat):
@ -128,7 +126,7 @@ class SignHandler(asynchat.async_chat):
serial_number, timestamp = line.split(":") serial_number, timestamp = line.split(":")
# TODO: Assert serial against regex # TODO: Assert serial against regex
revocation = crypto.Revoked() revocation = crypto.Revoked()
revocation.set_rev_date(datetime.fromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii")) revocation.set_rev_date(datetime.utcfromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii"))
revocation.set_reason(b"keyCompromise") revocation.set_reason(b"keyCompromise")
revocation.set_serial(serial_number.encode("ascii")) revocation.set_serial(serial_number.encode("ascii"))
crl.add_revoked(revocation) crl.add_revoked(revocation)
@ -137,7 +135,7 @@ class SignHandler(asynchat.async_chat):
self.server.certificate, self.server.certificate,
self.server.private_key, self.server.private_key,
crypto.FILETYPE_PEM, crypto.FILETYPE_PEM,
self.server.revocation_list_lifetime)) config.REVOCATION_LIST_LIFETIME))
elif cmd == "ocsp-request": elif cmd == "ocsp-request":
NotImplemented # TODO: Implement OCSP NotImplemented # TODO: Implement OCSP
@ -147,7 +145,7 @@ class SignHandler(asynchat.async_chat):
for e in request.get_extensions(): for e in request.get_extensions():
key = e.get_short_name().decode("ascii") key = e.get_short_name().decode("ascii")
if key not in EXTENSION_WHITELIST: if key not in constants.EXTENSION_WHITELIST:
raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key) raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key)
# TODO: Potential exploits during PEM parsing? # TODO: Potential exploits during PEM parsing?
@ -155,10 +153,10 @@ class SignHandler(asynchat.async_chat):
self.server.private_key, self.server.private_key,
self.server.certificate, self.server.certificate,
request, request,
basic_constraints=self.server.basic_constraints, basic_constraints=config.CERTIFICATE_BASIC_CONSTRAINTS,
key_usage=self.server.key_usage, key_usage=config.CERTIFICATE_KEY_USAGE_FLAGS,
extended_key_usage=self.server.extended_key_usage, extended_key_usage=config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
lifetime=self.server.lifetime) lifetime=config.CERTIFICATE_LIFETIME)
self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
else: else:
raise NotImplementedError("Unknown command: %s" % cmd) raise NotImplementedError("Unknown command: %s" % cmd)
@ -175,26 +173,23 @@ class SignHandler(asynchat.async_chat):
class SignServer(asyncore.dispatcher): class SignServer(asyncore.dispatcher):
def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage, revocation_list_lifetime): def __init__(self):
asyncore.dispatcher.__init__(self) asyncore.dispatcher.__init__(self)
# Bind to sockets # Bind to sockets
if os.path.exists(socket_path): if os.path.exists(config.SIGNER_SOCKET_PATH):
os.unlink(socket_path) os.unlink(config.SIGNER_SOCKET_PATH)
os.umask(0o007) os.umask(0o007)
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.bind(socket_path) self.bind(config.SIGNER_SOCKET_PATH)
self.listen(5) self.listen(5)
# Load CA private key and certificate
self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read())
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read())
self.lifetime = lifetime
self.revocation_list_lifetime = revocation_list_lifetime
self.basic_constraints = basic_constraints
self.key_usage = key_usage
self.extended_key_usage = extended_key_usage
# Load CA private key and certificate
self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM,
open(config.AUTHORITY_PRIVATE_KEY_PATH).read())
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM,
open(config.AUTHORITY_CERTIFICATE_PATH).read())
# Perhaps perform chroot as well, currently results in # Perhaps perform chroot as well, currently results in
# (<class 'OpenSSL.crypto.Error'>:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded') # (<class 'OpenSSL.crypto.Error'>:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded')

View File

@ -0,0 +1,27 @@
insert into log (
created,
facility,
level,
severity,
message,
module,
func,
lineno,
exception,
process,
thread,
thread_name
) values (
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s,
%s
);

View File

@ -0,0 +1,14 @@
create table if not exists log (
created datetime,
facility varchar(30),
level int,
severity varchar(10),
message text,
module varchar(20),
func varchar(20),
lineno int,
exception text,
process int,
thread text,
thread_name text
)

View File

@ -0,0 +1,9 @@
insert into tag (
`cn`,
`key`,
`value`
) values (
%s,
%s,
%s
)

View File

View File

@ -0,0 +1,27 @@
insert into log (
created,
facility,
level,
severity,
message,
module,
func,
lineno,
exception,
process,
thread,
thread_name
) values (
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?,
?
);

View File

@ -0,0 +1,14 @@
create table if not exists log (
created datetime,
facility varchar(30),
level int,
severity varchar(10),
message text,
module varchar(20),
func varchar(20),
lineno int,
exception text,
process int,
thread text,
thread_name text
)

View File

@ -0,0 +1,3 @@
delete from tag
where id = ?
limit 1

View File

@ -0,0 +1,9 @@
insert into tag (
`cn`,
`key`,
`value`
) values (
?,
?,
?
);

View File

@ -0,0 +1,16 @@
select
device_tag.id as `id`,
tag.key as `key`,
tag.value as `value`,
device.cn as `cn`
from
device_tag
join
tag
on
device_tag.tag_id = tag.id
join
device
on
device_tag.device_id = device.id

View File

@ -0,0 +1,36 @@
create table if not exists `tag` (
`id` integer primary key,
`cn` varchar(255) not null,
`key` varchar(255) not null,
`value` varchar(255) not null
);
create table if not exists `tag_properties` (
`id` integer primary key,
`tag_key` varchar(255) not null,
`tag_value` varchar(255) not null,
`property_key` varchar(255) not null,
`property_value` varchar(255) not null
);
/*
create table if not exists `device_tag` (
`id` int(11) not null,
`device_id` varchar(45) not null,
`tag_id` varchar(45) not null,
`attached` timestamp null default current_timestamp,
primary key (`id`)
);
create table if not exists `device` (
`id` int(11) not null,
`created` timestamp not null default current_timestamp,
`cn` varchar(255) not null,
`product_model` varchar(50) not null,
`product_serial` varchar(50) default null,
`hardware_address` varchar(17) unique not null,
primary key (`id`)
);
*/

View File

@ -0,0 +1,4 @@
update `tag`
set `value` = ?
where `id` = ?
limit 1

View File

@ -29,12 +29,6 @@ img {
max-height: 100%; max-height: 100%;
} }
ul {
list-style: none;
margin: 1em 0;
padding: 0;
}
#pending_requests .notify { #pending_requests .notify {
display: none; display: none;
} }
@ -142,7 +136,17 @@ pre {
margin: 0 auto; margin: 0 auto;
} }
#container li { #signed ul,
#requests ul,
#log ul {
list-style: none;
margin: 1em 0;
padding: 0;
}
#signed li,
#requests li,
#log li {
margin: 4px 0; margin: 4px 0;
padding: 4px 0; padding: 4px 0;
clear: both; clear: both;
@ -164,7 +168,8 @@ pre {
.icon{ .icon{
background-size: 24px; background-size: 24px;
padding-left: 36px; background-position: 6px 2px;
padding-left: 32px;
background-repeat: no-repeat; background-repeat: no-repeat;
display: block; display: block;
vertical-align: text-bottom; vertical-align: text-bottom;
@ -172,7 +177,7 @@ pre {
} }
#log_entries li span.icon { #log_entries li span.icon {
background-size: 32px; background-size: 24px;
padding-left: 42px; padding-left: 42px;
padding-top: 2px; padding-top: 2px;
padding-bottom: 2px; padding-bottom: 2px;
@ -180,7 +185,8 @@ pre {
.tags .tag { .tags .tag {
display: inline; display: inline;
background-size: 32px; background-size: 24px;
background-position: 0 4px;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
padding-right: 1em; padding-right: 1em;
@ -199,25 +205,25 @@ select {
} }
.icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); } .icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); }
.icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); } .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); }
.icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); } .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); }
.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); } .icon.warning { background-image: url("../img/iconmonstr-warning-8.svg"); }
.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); } .icon.info { background-image: url("../img/iconmonstr-info-8.svg"); }
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); } .icon.revoke { background-image: url("../img/iconmonstr-x-mark-8.svg"); }
.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); } .icon.download { background-image: url("../img/iconmonstr-download-12.svg"); }
.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); } .icon.sign { background-image: url("../img/iconmonstr-pen-14.svg"); }
.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); } .icon.search { background-image: url("../img/iconmonstr-magnifier-4.svg"); }
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); } .icon.phone { background-image: url("../img/iconmonstr-mobile-phone-7.svg"); }
.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); } .icon.location { background-image: url("../img/iconmonstr-compass-7.svg"); }
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); } .icon.room { background-image: url("../img/iconmonstr-home-7.svg"); }
.icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); } .icon.serial { background-image: url("../img/iconmonstr-barcode-4.svg"); }
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); } .icon.wireless { background-image: url("../img/iconmonstr-wireless-6.svg"); }
.icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); } .icon.password { background-image: url("../img/iconmonstr-lock-3.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

@ -1,15 +0,0 @@
<?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="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="barcode-4-icon" fill-rule="evenodd" clip-rule="evenodd" d="M117,331.114V181.956h23.215v149.158H117z M162.318,331.114
V181.956h27.75v149.158H162.318z M211.979,331.114V181.956h13.611v149.16L211.979,331.114z M297.614,331.114V181.956h13.61v149.16
L297.614,331.114z M247.827,331.114l-0.001-149.158h27.749v149.16L247.827,331.114z M333.566,331.114v-149.16h23.217v149.16H333.566
z M377.978,331.114v-149.16L395,181.956v149.158H377.978z M165.095,141.45H241v-30h-75.905V141.45z M345.566,111.45H269v30h76.566
V111.45z M241,400.55v-30h-75.905v30H241z M462,224.863h-30v68h30V224.863z M373.566,141.45H432v55.413h30V111.45h-88.434V141.45z
M345.566,370.55H269v30h76.566V370.55z M137.095,370.55H80v-49.687H50v79.687h87.095V370.55z M432,320.863v49.687h-58.434v30H462
v-79.687H432z M50,292.863h30v-76H50V292.863z M80,188.863V141.45h57.095v-30H50v77.413H80z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 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="M4 16v-8h2v8h-2zm12 0v-8h2v8h-2zm-9 0v-8h1v8h-1zm2 0v-8h2v8h-2zm3 0v-8h1v8h-1zm2 0v-8h1v8h-1zm5 0v-8h1v8h-1zm1-10h2v2h2v-4h-4v2zm-18 2v-2h2v-2h-4v4h2zm2 10h-2v-2h-2v4h4v-2zm18-2v2h-2v2h4v-4h-2zm-20-6h-2v4h2v-4zm22 0h-2v4h2v-4zm-13-6h-5v2h5v-2zm7 0h-5v2h5v-2zm-7 14h-5v2h5v-2zm7 0h-5v2h5v-2z"/></svg>

After

Width:  |  Height:  |  Size: 391 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="M24 2v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2zm1 11.729l.855-.791c1 .484 1.635.852 2.76 1.654 2.113-2.399 3.511-3.616 6.106-5.231l.279.64c-2.141 1.869-3.709 3.949-5.967 7.999-1.393-1.64-2.322-2.686-4.033-4.271z"/></svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@ -1,35 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.5 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="M18.625 19.46c-.264 1.696-.97 3.247-2.1 4.54-.065-.461-.254-.908-.433-1.266-.409.216-.899.33-1.328.33l-.265-.016c1.199-1.25 1.791-2.544 1.965-4.27.281.079.623.12 1.053.12.415.284.578.46 1.108.562zm4.875 3.589c-1.197-1.248-1.782-2.549-1.957-4.271-.283.079-.625.122-1.061.122-.414.285-.587.461-1.106.561.264 1.697.97 3.247 2.099 4.54.065-.461.254-.908.433-1.266.51.269 1.131.372 1.592.314zm-4.992-4.829c-.704-.494-.552-.447-1.423-.444h-.002c-.362 0-.685-.225-.794-.557-.267-.797-.176-.673-.879-1.163-.302-.208-.418-.577-.307-.9.273-.793.273-.641 0-1.438-.109-.32.002-.691.307-.9.703-.488.611-.365.879-1.163.109-.333.432-.557.794-.557h.002c.87.002.718.052 1.423-.444.146-.102.318-.154.492-.154s.346.052.492.154c.704.495.552.447 1.423.444h.001c.363 0 .686.224.797.557.266.796.172.673.877 1.163.223.153.348.397.348.65l-.042.25c-.272.793-.272.641 0 1.438.111.317 0 .69-.306.9-.705.489-.611.366-.877 1.163-.205.614-.656.555-1.18.555-.463 0-.465.042-1.041.446-.293.207-.691.207-.984 0zm.492-1.814c1.088 0 1.969-.882 1.969-1.969 0-1.087-.881-1.969-1.969-1.969s-1.969.881-1.969 1.969c0 1.087.881 1.969 1.969 1.969zm-4.674 1.333c-1.675 1.058-3.561 2.247-3.952 2.493-.043-.772-.329-1.492-.828-2.084-.058-.074-5.813-7.4-7.222-9.204-1.109-1.42-.06-2.934 1.23-2.934 1.694 0 2.369 2.207.894 3.163l1.163 1.486 8.053-4.551c1.396-1.032 1.79-2.938.914-4.434-.605-1.032-1.726-1.674-2.924-1.674-.475 0-.936.098-1.373.292l-8.264 4.227c-1.301.624-2.017 1.846-2.017 3.147 0 .816.282 1.663.877 2.412 1.444 1.815 7.261 9.253 7.319 9.328.862 1.147.017 2.753-1.376 2.753-.989 0-1.516-.705-1.667-1.308-.176-.705.069-1.407.773-1.858l-1.141-1.458c-.855.602-1.4 1.515-1.507 2.536-.228 2.174 1.504 3.929 3.557 3.929.63 0 1.255-.174 1.807-.502l5.485-3.458c.264-.415.529-1.431.199-2.301zm-3.319-15.755c.203-.095.431-.145.659-.145.577 0 1.082.305 1.349.816.338.645.244 1.59-.594 2.071l-5.307 3.006c0-1.191-.581-2.284-1.57-2.952l5.463-2.796zm1.987 6.267l1.725 2.117c.348-.525.858-.921 1.46-1.121l-1.562-1.916-1.623.92zm-2.886 8.043l2.871-1.628-.633-.825-2.863 1.661.625.792zm1.842-6.987l-5.012 2.943.624.792 5.026-2.951-.638-.784zm1.286 1.597l-5.035 2.965.624.792 5.049-2.974-.638-.783z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.7 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="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.608 9.476l-1.608-5.476-1.611 5.477c-.429.275-.775.658-1.019 1.107l-5.37 1.416 5.37 1.416c.243.449.589.833 1.019 1.107l1.611 5.477 1.618-5.479c.428-.275.771-.659 1.014-1.109l5.368-1.412-5.368-1.413c-.244-.452-.592-.836-1.024-1.111zm-1.608 4.024c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm5.25 3.75l-2.573-1.639c.356-.264.67-.579.935-.934l1.638 2.573zm-2.641-8.911l2.64-1.588-1.588 2.639c-.29-.407-.645-.761-1.052-1.051zm-5.215 7.325l-2.644 1.586 1.589-2.641c.29.408.646.764 1.055 1.055zm-1.005-6.34l-1.638-2.573 2.573 1.638c-.357.264-.672.579-.935.935z"/></svg>

After

Width:  |  Height:  |  Size: 837 B

View File

@ -1,29 +0,0 @@
<?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="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="download-12-icon" d="M462,246.575c0,44.318-35.928,80.246-80.246,80.246H331.58v-38.119
c-0.998-43.379,40.92-44.379,59.67-46.379c-27.168-33.334-70.918-48.244-104.611-48.244c-66.546,0-108.17,39.104-108.17,104.808
v27.935h-48.223C85.928,326.821,50,290.894,50,246.575c0-40.982,30.729-74.766,70.396-79.623
c2.891-42.287,49.035-66.355,85.217-45.898c19.236-30.605,53.297-50.953,92.115-50.953c57.107,0,103.932,44.033,108.375,100
C438.516,180.413,462,210.747,462,246.575z M301.58,288.702c0-30.761,6.053-48.484,31.926-56.837
c-20.066-8.452-125.037-28.815-125.037,67.021c0,32.187,0,58.909,0,58.909h-37.408l83.963,84.104l83.965-84.104H301.58
C301.58,357.796,301.58,315.59,301.58,288.702z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 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="M6 13h4v-7h4v7h4l-6 6-6-6zm16-1c0 5.514-4.486 10-10 10s-10-4.486-10-10 4.486-10 10-10 10 4.486 10 10zm2 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -1,21 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 982 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="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@ -1,12 +0,0 @@
<?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="error-4-icon" d="M324.76,90L422,187.24v137.52L324.76,422H187.24L90,324.76V187.24L187.24,90H324.76 M341.328,50H170.672
L50,170.672v170.656L170.672,462h170.656L462,341.328V170.672L341.328,50L341.328,50z M228.55,135.812h54.9v166.5h-54.9V135.812z
M256,388.188c-16.362,0-29.625-13.264-29.625-29.625c0-16.362,13.263-29.627,29.625-29.627c16.361,0,29.625,13.265,29.625,29.627
C285.625,374.924,272.361,388.188,256,388.188z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 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.143 2l5.857 5.858v8.284l-5.857 5.858h-8.286l-5.857-5.858v-8.284l5.857-5.858h8.286zm.828-2h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-6.471 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -1,11 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 786 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="M4 24h-2v-24h2v24zm18-21.387s-1.621 1.43-3.754 1.43c-3.36 0-3.436-2.895-7.337-2.895-2.108 0-4.075.98-4.909 1.694v12.085c1.184-.819 2.979-1.681 4.923-1.681 3.684 0 4.201 2.754 7.484 2.754 2.122 0 3.593-1.359 3.593-1.359v-12.028z"/></svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 836 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 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -1,18 +0,0 @@
<?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="info-6-icon" d="M256,90.002c91.74,0,166,74.241,166,165.998c0,91.739-74.245,165.998-166,165.998
c-91.738,0-166-74.242-166-165.998C90,164.259,164.243,90.002,256,90.002 M256,50.002C142.229,50.002,50,142.228,50,256
c0,113.769,92.229,205.998,206,205.998c113.77,0,206-92.229,206-205.998C462,142.228,369.77,50.002,256,50.002L256,50.002z
M252.566,371.808c-28.21,9.913-51.466-1.455-46.801-28.547c4.667-27.098,31.436-85.109,35.255-96.079
c3.816-10.97-3.502-13.977-11.346-9.513c-4.524,2.61-11.248,7.841-17.02,12.925c-1.601-3.223-3.852-6.906-5.542-10.433
c9.419-9.439,25.164-22.094,43.803-26.681c22.27-5.497,59.492,3.29,43.494,45.858c-11.424,30.34-19.503,51.276-24.594,66.868
c-5.088,15.598,0.955,18.868,9.863,12.791c6.959-4.751,14.372-11.214,19.806-16.226c2.515,4.086,3.319,5.389,5.806,10.084
C295.857,342.524,271.182,365.151,252.566,371.808z M311.016,184.127c-12.795,10.891-31.76,10.655-42.37-0.532
c-10.607-11.181-8.837-29.076,3.955-39.969c12.794-10.89,31.763-10.654,42.37,0.525
C325.577,155.337,323.809,173.231,311.016,184.127z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 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="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.033 16.01c.564-1.789 1.632-3.932 1.821-4.474.273-.787-.211-1.136-1.74.209l-.34-.64c1.744-1.897 5.335-2.326 4.113.613-.763 1.835-1.309 3.074-1.621 4.03-.455 1.393.694.828 1.819-.211.153.25.203.331.356.619-2.498 2.378-5.271 2.588-4.408-.146zm4.742-8.169c-.532.453-1.32.443-1.761-.022-.441-.465-.367-1.208.164-1.661.532-.453 1.32-.442 1.761.022.439.466.367 1.209-.164 1.661z"/></svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@ -1,15 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.3 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="M12.451 17.337l-2.451 2.663h-2v2h-2v2h-6v-1.293l7.06-7.06c-.214-.26-.413-.533-.599-.815l-6.461 6.461v-2.293l6.865-6.949c1.08 2.424 3.095 4.336 5.586 5.286zm11.549-9.337c0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8 8 3.582 8 8zm-3-3c0-1.104-.896-2-2-2s-2 .896-2 2 .896 2 2 2 2-.896 2-2z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@ -1,13 +0,0 @@
<?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="lock-3-icon" d="M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667
v62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108
V253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773
c4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,27 +0,0 @@
<?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="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="magnifier-4-icon" d="M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094
c0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641
c31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764
c0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352
S103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862
C265.221,150.329,188.719,144.834,138.455,188.504z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 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="M23.111 20.058l-4.977-4.977c.965-1.52 1.523-3.322 1.523-5.251 0-5.42-4.409-9.83-9.829-9.83-5.42 0-9.828 4.41-9.828 9.83s4.408 9.83 9.829 9.83c1.834 0 3.552-.505 5.022-1.383l5.021 5.021c2.144 2.141 5.384-1.096 3.239-3.24zm-20.064-10.228c0-3.739 3.043-6.782 6.782-6.782s6.782 3.042 6.782 6.782-3.043 6.782-6.782 6.782-6.782-3.043-6.782-6.782zm2.01-1.764c1.984-4.599 8.664-4.066 9.922.749-2.534-2.974-6.993-3.294-9.922-.749z"/></svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -1,17 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.5 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="M5 6c-1.104 0-2 .896-2 2v14c0 1.104.896 2 2 2h8c1.104 0 2-.896 2-2v-14c0-1.104-.896-2-2-2h-8zm2 15h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm0-3h-8v-7h8v7zm0-11.688c.944-.001 1.889.359 2.608 1.08.721.72 1.082 1.664 1.082 2.606h1.554c-.001-1.341-.514-2.684-1.538-3.707-1.025-1.022-2.365-1.533-3.706-1.532v1.553zm0-2.718c1.639-.001 3.277.623 4.53 1.874 1.251 1.25 1.877 2.892 1.878 4.531h1.592c-.001-2.047-.782-4.096-2.345-5.658-1.562-1.562-3.609-2.341-5.655-2.34v1.593z"/></svg>

After

Width:  |  Height:  |  Size: 613 B

View File

@ -1,13 +0,0 @@
<?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="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<path id="pen-10-icon" d="M244.558,199.493l67.827,67.826l-73.17,134.531c0,0-90.805,23.4-147.694,60.027l-14.185-14.182
l68.113-68.105c5.975-5.982,13.726-9.773,22.11-10.807c4.642-0.582,9.128-2.621,12.696-6.205c8.538-8.547,8.546-22.4-0.002-30.951
c-8.549-8.543-22.407-8.543-30.959-0.002c-3.573,3.572-5.623,8.061-6.199,12.693c-1.028,8.371-4.834,16.15-10.8,22.117
l-68.104,68.105L50,420.354c37.028-57.496,60.021-147.693,60.021-147.693L244.558,199.493z M315.896,50.122
c-22.784,44.143-53.014,100-53.014,100l98.872,98.869c0,0,55.909-30.086,100.246-52.766L315.896,50.122z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1016 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="M12.014 6.54s2.147-3.969 3.475-6.54l8.511 8.511c-2.583 1.321-6.556 3.459-6.556 3.459l-5.43-5.43zm-8.517 6.423s-1.339 5.254-3.497 8.604l.827.826 3.967-3.967c.348-.348.569-.801.629-1.288.034-.27.153-.532.361-.74.498-.498 1.306-.498 1.803 0 .498.499.498 1.305 0 1.803-.208.209-.469.328-.74.361-.488.061-.94.281-1.288.63l-3.967 3.968.826.84c3.314-2.133 8.604-3.511 8.604-3.511l4.262-7.837-3.951-3.951-7.836 4.262z"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -1,25 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.1 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="M10.605 0h-10.604v10.609l13.39 13.391 10.609-10.605-13.395-13.395zm-7.019 6.414c-.781-.782-.781-2.047 0-2.828.782-.781 2.048-.781 2.828-.002.782.783.782 2.048 0 2.83-.781.781-2.046.781-2.828 0zm6.823 8.947l-4.243-4.242.708-.708 4.243 4.243-.708.707zm4.949.707l-7.07-7.071.707-.707 7.071 7.071-.708.707zm2.121-2.121l-7.071-7.071.707-.707 7.071 7.071-.707.707z"/></svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@ -1,18 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.6 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="M19 7.001c0 3.865-3.134 7-7 7s-7-3.135-7-7c0-3.867 3.134-7.001 7-7.001s7 3.134 7 7.001zm-1.598 7.18c-1.506 1.137-3.374 1.82-5.402 1.82-2.03 0-3.899-.685-5.407-1.822-4.072 1.793-6.593 7.376-6.593 9.821h24c0-2.423-2.6-8.006-6.598-9.819z"/></svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@ -1,11 +0,0 @@
<?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="warning-6-icon" d="M239.939,231.352h32.121v97.421h-32.121V231.352z M256,379.019c-9.574,0-17.334-7.761-17.334-17.334
c0-9.574,7.76-17.335,17.334-17.335c9.573,0,17.334,7.761,17.334,17.335C273.334,371.258,265.573,379.019,256,379.019z M256,78.07
L50,434.873h412L256,78.07z M256,158.07l136.718,236.803H119.282L256,158.07z"/>
</svg>

Before

Width:  |  Height:  |  Size: 970 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="M12 5.177l8.631 15.823h-17.262l8.631-15.823zm0-4.177l-12 22h24l-12-22zm-1 9h2v6h-2v-6zm1 9.75c-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25 1.25.56 1.25 1.25-.561 1.25-1.25 1.25z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -1,16 +0,0 @@
<?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="wireless-6-icon" d="M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35
c-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796
c27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936
c-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599
c11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269
s-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297
c0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,21 +0,0 @@
<?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="x-mark-5-icon" d="M432.546,133.462L367.133,76.39L254.078,210.715L140.967,73.702l-61.513,65.068
c33.791,43.885,78.146,89.797,123.688,132.465L82.993,413.987l19.865,22.629c29.251-20.31,87.839-65.578,150.312-120.092
c63.662,55.812,122.861,101.336,151.301,121.773l21.438-19.443L303.804,270.95C352.439,225.709,399.308,177.442,432.546,133.462z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1001 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="M24 3.752l-4.423-3.752-7.771 9.039-7.647-9.008-4.159 4.278c2.285 2.885 5.284 5.903 8.362 8.708l-8.165 9.447 1.343 1.487c1.978-1.335 5.981-4.373 10.205-7.958 4.304 3.67 8.306 6.663 10.229 8.006l1.449-1.278-8.254-9.724c3.287-2.973 6.584-6.354 8.831-9.245z"/></svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -14,11 +14,12 @@
<body> <body>
<nav id="menu"> <nav id="menu">
<ul class="container"> <ul class="container">
<li data-section="requests">Requests</li> <li data-section="about">Profile</li>
<li data-section="signed">Signed</li> <li id="section-requests" data-section="requests" style="display:none;">Requests</li>
<li data-section="revoked">Revoked</li> <li id="section-signed" data-section="signed" style="display:none;">Signed</li>
<li data-section="config">Configuration</li> <li id="section-revoked" data-section="revoked" style="display:none;">Revoked</li>
<li data-section="log">Log</li> <li id="section-config" data-section="config" style="display:none;">Configuration</li>
<li id="section-log" data-section="log" style="display:none;">Log</li>
</ul> </ul>
</nav> </nav>
<div id="container" class="container"> <div id="container" class="container">

View File

@ -67,7 +67,6 @@ function onRequestSubmitted(e) {
url: "/api/request/" + e.data + "/", url: "/api/request/" + e.data + "/",
dataType: "json", dataType: "json",
success: function(request, status, xhr) { success: function(request, status, xhr) {
console.info(request);
$("#pending_requests").prepend( $("#pending_requests").prepend(
nunjucks.render('views/request.html', { request: request })); nunjucks.render('views/request.html', { request: request }));
} }
@ -75,12 +74,12 @@ function onRequestSubmitted(e) {
} }
function onRequestDeleted(e) { function onRequestDeleted(e) {
console.log("Removing deleted request #" + e.data); console.log("Removing deleted request", e.data);
$("#request_" + e.data).remove(); $("#request-" + e.data.replace("@", "--").replace(".", "-")).remove();
} }
function onClientUp(e) { function onClientUp(e) {
console.log("Adding security association:" + e.data); console.log("Adding security association:", e.data);
var lease = JSON.parse(e.data); var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('views/status.html', { $status.html(nunjucks.render('views/status.html', {
@ -93,7 +92,7 @@ function onClientUp(e) {
} }
function onClientDown(e) { function onClientDown(e) {
console.log("Removing security association:" + e.data); console.log("Removing security association:", e.data);
var lease = JSON.parse(e.data); var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('views/status.html', { $status.html(nunjucks.render('views/status.html', {
@ -107,7 +106,9 @@ function onClientDown(e) {
function onRequestSigned(e) { function onRequestSigned(e) {
console.log("Request signed:", e.data); console.log("Request signed:", e.data);
$("#request_" + e.data).slideUp("normal", function() { $(this).remove(); });
$("#request-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
$("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
$.ajax({ $.ajax({
method: "GET", method: "GET",
@ -121,13 +122,14 @@ function onRequestSigned(e) {
}); });
} }
function onCertificateRevoked(e) { function onCertificateRevoked(e) {
console.log("Removing revoked certificate #" + e.data); console.log("Removing revoked certificate", e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); }); $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
} }
function onTagAdded(e) { function onTagAdded(e) {
console.log("Tag added #" + e.data); console.log("Tag added", e.data);
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "/api/tag/" + e.data + "/", url: "/api/tag/" + e.data + "/",
@ -143,12 +145,12 @@ function onTagAdded(e) {
} }
function onTagRemoved(e) { function onTagRemoved(e) {
console.log("Tag removed #" + e.data); console.log("Tag removed", e.data);
$("#tag_" + e.data).remove(); $("#tag_" + e.data).remove();
} }
function onTagUpdated(e) { function onTagUpdated(e) {
console.log("Tag updated #" + e.data); console.log("Tag updated", e.data);
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "/api/tag/" + e.data + "/", url: "/api/tag/" + e.data + "/",
@ -175,32 +177,49 @@ $(document).ready(function() {
$("#container").html(nunjucks.render('views/error.html', { message: msg })); $("#container").html(nunjucks.render('views/error.html', { message: msg }));
}, },
success: function(session, status, xhr) { success: function(session, status, xhr) {
console.info("Opening EventSource from:", session.event_channel); $("#login").hide();
var source = new EventSource(session.event_channel);
source.onmessage = function(event) {
console.log("Received server-sent event:", event);
}
source.addEventListener("log-entry", onLogEntry);
source.addEventListener("up-client", onClientUp);
source.addEventListener("down-client", onClientDown);
source.addEventListener("request-deleted", onRequestDeleted);
source.addEventListener("request-submitted", onRequestSubmitted);
source.addEventListener("request-signed", onRequestSigned);
source.addEventListener("certificate-revoked", onCertificateRevoked);
source.addEventListener("tag-added", onTagAdded);
source.addEventListener("tag-removed", onTagRemoved);
source.addEventListener("tag-updated", onTagUpdated);
/** /**
* Render authority views * Render authority views
**/ **/
$("#container").html(nunjucks.render('views/authority.html', { session: session, window: window })); $("#container").html(nunjucks.render('views/authority.html', { session: session, window: window }));
console.info("Swtiching to requests section");
$("section").hide(); if (session.authority) {
$("section#requests").show(); $("#log input").each(function(i, e) {
console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true");
e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked;
});
$("#log input").change(function() {
localStorage[this.id] = this.checked;
});
console.info("Opening EventSource from:", session.authority.events);
var source = new EventSource(session.authority.events);
source.onmessage = function(event) {
console.log("Received server-sent event:", event);
}
source.addEventListener("log-entry", onLogEntry);
source.addEventListener("up-client", onClientUp);
source.addEventListener("down-client", onClientDown);
source.addEventListener("request-deleted", onRequestDeleted);
source.addEventListener("request-submitted", onRequestSubmitted);
source.addEventListener("request-signed", onRequestSigned);
source.addEventListener("certificate-revoked", onCertificateRevoked);
source.addEventListener("tag-added", onTagAdded);
source.addEventListener("tag-removed", onTagRemoved);
source.addEventListener("tag-updated", onTagUpdated);
console.info("Swtiching to requests section");
$("section").hide();
$("section#requests").show();
$("#section-revoked").show();
$("#section-signed").show();
$("#section-requests").show();
}
$("nav#menu li").click(function(e) { $("nav#menu li").click(function(e) {
$("section").hide(); $("section").hide();
@ -231,88 +250,97 @@ $(document).ready(function() {
}); });
console.log("Features enabled:", session.features);
if (session.features.tagging) {
$.ajax({ console.info("Tagging enabled");
method: "GET", $("#section-config").show();
url: "/api/config/", $.ajax({
dataType: "json", method: "GET",
success: function(configuration, status, xhr) { url: "/api/config/",
console.info("Appending " + configuration.length + " configuration items"); dataType: "json",
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); success: function(configuration, status, xhr) {
/** console.info("Appending", configuration.length, "configuration items");
* Fetch tags for certificates $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
*/ /**
$.ajax({ * Fetch tags for certificates
method: "GET", */
url: "/api/tag/", $.ajax({
dataType: "json", method: "GET",
success:function(tags, status, xhr) { url: "/api/tag/",
console.info("Got", tags.length, "tags"); dataType: "json",
for (var j = 0; j < tags.length; j++) { success:function(tags, status, xhr) {
// TODO: Deduplicate console.info("Got", tags.length, "tags");
$tag = $("<span id=\"tag_" + tags[j].id + "\" title=\"" + tags[j].key + "=" + tags[j].value + "\" class=\"" + tags[j].key.replace(/\./g, " ") + " icon tag\" data-id=\""+tags[j].id+"\" data-key=\"" + tags[j].key + "\">" + tags[j].value + "</span>"); $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
console.info("Inserting tag", tags[j], $tag); for (var j = 0; j < tags.length; j++) {
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" "); // TODO: Deduplicate
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag); $tag = $("<span id=\"tag_" + tags[j].id + "\" title=\"" + tags[j].key + "=" + tags[j].value + "\" class=\"" + tags[j].key.replace(/\./g, " ") + " icon tag\" data-id=\""+tags[j].id+"\" data-key=\"" + tags[j].key + "\">" + tags[j].value + "</span>");
$tag.click(onTagClicked); console.info("Inserting tag", tags[j], $tag);
$("#tags_autocomplete").prepend("<option value=\"" + tags[j].id + "\">" + tags[j].key + "='" + tags[j].value + "'</option>"); $tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
$tag.click(onTagClicked);
$("#tags_autocomplete").prepend("<option value=\"" + tags[j].id + "\">" + tags[j].key + "='" + tags[j].value + "'</option>");
}
} }
} });
}); }
} });
}); }
/** /**
* Fetch leases associated with certificates * Fetch leases associated with certificates
*/ */
$.ajax({ if (session.features.leases) {
method: "GET", $.ajax({
url: "/api/lease/", method: "GET",
dataType: "json", url: "/api/lease/",
success: function(leases, status, xhr) { dataType: "json",
console.info("Got leases:", leases); success: function(leases, status, xhr) {
for (var j = 0; j < leases.length; j++) { console.info("Got leases:", leases);
var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status"); for (var j = 0; j < leases.length; j++) {
if (!$status.length) { var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status");
console.info("Detected rogue client:", leases[j]); if (!$status.length) {
continue; console.info("Detected rogue client:", leases[j]);
continue;
}
$status.html(nunjucks.render('views/status.html', {
lease: {
address: leases[j].address,
age: (new Date() - new Date(leases[j].released)) / 1000,
identity: leases[j].identity,
acquired: new Date(leases[j].acquired).toLocaleString(),
released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null
}}));
} }
$status.html(nunjucks.render('views/status.html', {
lease: {
address: leases[j].address,
age: (new Date() - new Date(leases[j].released)) / 1000,
identity: leases[j].identity,
acquired: new Date(leases[j].acquired).toLocaleString(),
released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null
}}));
}
} }
}); });
return; }
/** /**
* Fetch log entries * Fetch log entries
*/ */
$.ajax({ if (session.features.logging) {
method: "GET", $("#section-log").show();
url: "/api/log/", $.ajax({
dataType: "json", method: "GET",
success:function(entries, status, xhr) { url: "/api/log/",
console.info("Got", entries.length, "log entries"); dataType: "json",
for (var j = 0; j < entries.length; j++) { success:function(entries, status, xhr) {
if ($("#log_level_" + entries[j].severity).prop("checked")) { console.info("Got", entries.length, "log entries");
$("#log_entries").append(nunjucks.render("views/logentry.html", { for (var j = 0; j < entries.length; j++) {
entry: { if ($("#log_level_" + entries[j].severity).prop("checked")) {
created: new Date(entries[j].created).toLocaleString("et-EE"), $("#log_entries").append(nunjucks.render("views/logentry.html", {
message: entries[j].message, entry: {
severity: entries[j].severity created: new Date(entries[j].created).toLocaleString("et-EE"),
} message: entries[j].message,
})); severity: entries[j].severity
}
}));
}
} }
} }
} });
}); }
} }
}); });
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-barcode-4-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-barcode-4.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"barcode-4-icon\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M117,331.114V181.956h23.215v149.158H117z M162.318,331.114\r\n\tV181.956h27.75v149.158H162.318z M211.979,331.114V181.956h13.611v149.16L211.979,331.114z M297.614,331.114V181.956h13.61v149.16\r\n\tL297.614,331.114z M247.827,331.114l-0.001-149.158h27.749v149.16L247.827,331.114z M333.566,331.114v-149.16h23.217v149.16H333.566\r\n\tz M377.978,331.114v-149.16L395,181.956v149.158H377.978z M165.095,141.45H241v-30h-75.905V141.45z M345.566,111.45H269v30h76.566\r\n\tV111.45z M241,400.55v-30h-75.905v30H241z M462,224.863h-30v68h30V224.863z M373.566,141.45H432v55.413h30V111.45h-88.434V141.45z\r\n\t M345.566,370.55H269v30h76.566V370.55z M137.095,370.55H80v-49.687H50v79.687h87.095V370.55z M432,320.863v49.687h-58.434v30H462\r\n\tv-79.687H432z M50,292.863h30v-76H50V292.863z M80,188.863V141.45h57.095v-30H50v77.413H80z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 16v-8h2v8h-2zm12 0v-8h2v8h-2zm-9 0v-8h1v8h-1zm2 0v-8h2v8h-2zm3 0v-8h1v8h-1zm2 0v-8h1v8h-1zm5 0v-8h1v8h-1zm1-10h2v2h2v-4h-4v2zm-18 2v-2h2v-2h-4v4h2zm2 10h-2v-2h-2v4h4v-2zm18-2v2h-2v2h4v-4h-2zm-20-6h-2v4h2v-4zm22 0h-2v4h2v-4zm-13-6h-5v2h5v-2zm7 0h-5v2h5v-2zm-7 14h-5v2h5v-2zm7 0h-5v2h5v-2z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -22,14 +22,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-calendar-6.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" style=\"enable-background:new 0 0 512 512;\" xml:space=\"preserve\">\r\n<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\r\n\tc-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\r\n\tc4.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\r\n\tc-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\r\n\tc4.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\r\n\t 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\r\n\tc-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\r\n\tc-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\r\n\tc0.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\r\n\tc12.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\r\n\tc4.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\r\n\tc-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\r\n\tc-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\r\n\t 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\r\n\tC346.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\r\n\tc-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\r\n\tc29.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\r\n\tC294.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\r\n\tc0,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\r\n\tc-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\r\n\tc-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\r\n\tl94.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\r\n\tc9.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\r\n\tL243.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\r\n\tc-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\r\n\tL227.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\r\n\tl10.713,13.597l86.679-51.048L281.516,237.182z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 2v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2zm1 11.729l.855-.791c1 .484 1.635.852 2.76 1.654 2.113-2.399 3.511-3.616 6.106-5.231l.279.64c-2.141 1.869-3.709 3.949-5.967 7.999-1.393-1.64-2.322-2.686-4.033-4.271z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -46,14 +46,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<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\r\n\tC90,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\r\n\t 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\r\n\tc6.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\r\n\tC208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74\r\n\tC309.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\r\n\tL256,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\r\n\tc4.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\r\n\t 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\r\n\tS278.406,268.376,278.406,256z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M18.625 19.46c-.264 1.696-.97 3.247-2.1 4.54-.065-.461-.254-.908-.433-1.266-.409.216-.899.33-1.328.33l-.265-.016c1.199-1.25 1.791-2.544 1.965-4.27.281.079.623.12 1.053.12.415.284.578.46 1.108.562zm4.875 3.589c-1.197-1.248-1.782-2.549-1.957-4.271-.283.079-.625.122-1.061.122-.414.285-.587.461-1.106.561.264 1.697.97 3.247 2.099 4.54.065-.461.254-.908.433-1.266.51.269 1.131.372 1.592.314zm-4.992-4.829c-.704-.494-.552-.447-1.423-.444h-.002c-.362 0-.685-.225-.794-.557-.267-.797-.176-.673-.879-1.163-.302-.208-.418-.577-.307-.9.273-.793.273-.641 0-1.438-.109-.32.002-.691.307-.9.703-.488.611-.365.879-1.163.109-.333.432-.557.794-.557h.002c.87.002.718.052 1.423-.444.146-.102.318-.154.492-.154s.346.052.492.154c.704.495.552.447 1.423.444h.001c.363 0 .686.224.797.557.266.796.172.673.877 1.163.223.153.348.397.348.65l-.042.25c-.272.793-.272.641 0 1.438.111.317 0 .69-.306.9-.705.489-.611.366-.877 1.163-.205.614-.656.555-1.18.555-.463 0-.465.042-1.041.446-.293.207-.691.207-.984 0zm.492-1.814c1.088 0 1.969-.882 1.969-1.969 0-1.087-.881-1.969-1.969-1.969s-1.969.881-1.969 1.969c0 1.087.881 1.969 1.969 1.969zm-4.674 1.333c-1.675 1.058-3.561 2.247-3.952 2.493-.043-.772-.329-1.492-.828-2.084-.058-.074-5.813-7.4-7.222-9.204-1.109-1.42-.06-2.934 1.23-2.934 1.694 0 2.369 2.207.894 3.163l1.163 1.486 8.053-4.551c1.396-1.032 1.79-2.938.914-4.434-.605-1.032-1.726-1.674-2.924-1.674-.475 0-.936.098-1.373.292l-8.264 4.227c-1.301.624-2.017 1.846-2.017 3.147 0 .816.282 1.663.877 2.412 1.444 1.815 7.261 9.253 7.319 9.328.862 1.147.017 2.753-1.376 2.753-.989 0-1.516-.705-1.667-1.308-.176-.705.069-1.407.773-1.858l-1.141-1.458c-.855.602-1.4 1.515-1.507 2.536-.228 2.174 1.504 3.929 3.557 3.929.63 0 1.255-.174 1.807-.502l5.485-3.458c.264-.415.529-1.431.199-2.301zm-3.319-15.755c.203-.095.431-.145.659-.145.577 0 1.082.305 1.349.816.338.645.244 1.59-.594 2.071l-5.307 3.006c0-1.191-.581-2.284-1.57-2.952l5.463-2.796zm1.987 6.267l1.725 2.117c.348-.525.858-.921 1.46-1.121l-1.562-1.916-1.623.92zm-2.886 8.043l2.871-1.628-.633-.825-2.863 1.661.625.792zm1.842-6.987l-5.012 2.943.624.792 5.026-2.951-.638-.784zm1.286 1.597l-5.035 2.965.624.792 5.049-2.974-.638-.783z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -70,14 +70,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"download-12-icon\" d=\"M462,246.575c0,44.318-35.928,80.246-80.246,80.246H331.58v-38.119\n\n\tc-0.998-43.379,40.92-44.379,59.67-46.379c-27.168-33.334-70.918-48.244-104.611-48.244c-66.546,0-108.17,39.104-108.17,104.808\n\n\tv27.935h-48.223C85.928,326.821,50,290.894,50,246.575c0-40.982,30.729-74.766,70.396-79.623\n\n\tc2.891-42.287,49.035-66.355,85.217-45.898c19.236-30.605,53.297-50.953,92.115-50.953c57.107,0,103.932,44.033,108.375,100\n\n\tC438.516,180.413,462,210.747,462,246.575z M301.58,288.702c0-30.761,6.053-48.484,31.926-56.837\n\n\tc-20.066-8.452-125.037-28.815-125.037,67.021c0,32.187,0,58.909,0,58.909h-37.408l83.963,84.104l83.965-84.104H301.58\n\n\tC301.58,357.796,301.58,315.59,301.58,288.702z\"/>\n\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.608 9.476l-1.608-5.476-1.611 5.477c-.429.275-.775.658-1.019 1.107l-5.37 1.416 5.37 1.416c.243.449.589.833 1.019 1.107l1.611 5.477 1.618-5.479c.428-.275.771-.659 1.014-1.109l5.368-1.412-5.368-1.413c-.244-.452-.592-.836-1.024-1.111zm-1.608 4.024c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm5.25 3.75l-2.573-1.639c.356-.264.67-.579.935-.934l1.638 2.573zm-2.641-8.911l2.64-1.588-1.588 2.639c-.29-.407-.645-.761-1.052-1.051zm-5.215 7.325l-2.644 1.586 1.589-2.641c.29.408.646.764 1.055 1.055zm-1.005-6.34l-1.638-2.573 2.573 1.638c-.357.264-.672.579-.935.935z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -94,14 +94,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"email-2-icon\" d=\"M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465\n\n\tL96.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\n\n\tl57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z\"/>\n\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M6 13h4v-7h4v7h4l-6 6-6-6zm16-1c0 5.514-4.486 10-10 10s-10-4.486-10-10 4.486-10 10-10 10 4.486 10 10zm2 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -118,14 +118,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"error-4-icon\" d=\"M324.76,90L422,187.24v137.52L324.76,422H187.24L90,324.76V187.24L187.24,90H324.76 M341.328,50H170.672\n\tL50,170.672v170.656L170.672,462h170.656L462,341.328V170.672L341.328,50L341.328,50z M228.55,135.812h54.9v166.5h-54.9V135.812z\n\t M256,388.188c-16.362,0-29.625-13.264-29.625-29.625c0-16.362,13.263-29.627,29.625-29.627c16.361,0,29.625,13.265,29.625,29.627\n\tC285.625,374.924,272.361,388.188,256,388.188z\"/>\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -142,14 +142,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"flag-3-icon\" d=\"M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642\r\n\tc-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\r\n\tc21.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\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M16.143 2l5.857 5.858v8.284l-5.857 5.858h-8.286l-5.857-5.858v-8.284l5.857-5.858h8.286zm.828-2h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-6.471 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -166,14 +166,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-4-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"home-4-icon\" d=\"M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492\n\n\tz M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z\"/>\n\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 24h-2v-24h2v24zm18-21.387s-1.621 1.43-3.754 1.43c-3.36 0-3.436-2.895-7.337-2.895-2.108 0-4.075.98-4.909 1.694v12.085c1.184-.819 2.979-1.681 4.923-1.681 3.684 0 4.201 2.754 7.484 2.754 2.122 0 3.593-1.359 3.593-1.359v-12.028z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -190,14 +190,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-6-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-7.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"info-6-icon\" d=\"M256,90.002c91.74,0,166,74.241,166,165.998c0,91.739-74.245,165.998-166,165.998\n\tc-91.738,0-166-74.242-166-165.998C90,164.259,164.243,90.002,256,90.002 M256,50.002C142.229,50.002,50,142.228,50,256\n\tc0,113.769,92.229,205.998,206,205.998c113.77,0,206-92.229,206-205.998C462,142.228,369.77,50.002,256,50.002L256,50.002z\n\t M252.566,371.808c-28.21,9.913-51.466-1.455-46.801-28.547c4.667-27.098,31.436-85.109,35.255-96.079\n\tc3.816-10.97-3.502-13.977-11.346-9.513c-4.524,2.61-11.248,7.841-17.02,12.925c-1.601-3.223-3.852-6.906-5.542-10.433\n\tc9.419-9.439,25.164-22.094,43.803-26.681c22.27-5.497,59.492,3.29,43.494,45.858c-11.424,30.34-19.503,51.276-24.594,66.868\n\tc-5.088,15.598,0.955,18.868,9.863,12.791c6.959-4.751,14.372-11.214,19.806-16.226c2.515,4.086,3.319,5.389,5.806,10.084\n\tC295.857,342.524,271.182,365.151,252.566,371.808z M311.016,184.127c-12.795,10.891-31.76,10.655-42.37-0.532\n\tc-10.607-11.181-8.837-29.076,3.955-39.969c12.794-10.89,31.763-10.654,42.37,0.525\n\tC325.577,155.337,323.809,173.231,311.016,184.127z\"/>\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M20 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -214,14 +214,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-2-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-8.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"key-2-icon\" stroke=\"#000000\" stroke-miterlimit=\"10\" d=\"M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872\r\n\tl-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\r\n\tl145.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\r\n\tc-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537\r\n\tC298.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\r\n\tc-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\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.033 16.01c.564-1.789 1.632-3.932 1.821-4.474.273-.787-.211-1.136-1.74.209l-.34-.64c1.744-1.897 5.335-2.326 4.113.613-.763 1.835-1.309 3.074-1.621 4.03-.455 1.393.694.828 1.819-.211.153.25.203.331.356.619-2.498 2.378-5.271 2.588-4.408-.146zm4.742-8.169c-.532.453-1.32.443-1.761-.022-.441-.465-.367-1.208.164-1.661.532-.453 1.32-.442 1.761.022.439.466.367 1.209-.164 1.661z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -238,14 +238,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-lock-3-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-3.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"lock-3-icon\" d=\"M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667\r\n\tv62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108\r\n\tV253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773\r\n\tc4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.451 17.337l-2.451 2.663h-2v2h-2v2h-6v-1.293l7.06-7.06c-.214-.26-.413-.533-.599-.815l-6.461 6.461v-2.293l6.865-6.949c1.08 2.424 3.095 4.336 5.586 5.286zm11.549-9.337c0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8 8 3.582 8 8zm-3-3c0-1.104-.896-2-2-2s-2 .896-2 2 .896 2 2 2 2-.896 2-2z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -262,14 +262,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"magnifier-4-icon\" d=\"M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094\n\n\tc0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641\n\n\tc31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764\n\n\tc0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352\n\n\tS103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862\n\n\tC265.221,150.329,188.719,144.834,138.455,188.504z\"/>\n\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M23.111 20.058l-4.977-4.977c.965-1.52 1.523-3.322 1.523-5.251 0-5.42-4.409-9.83-9.829-9.83-5.42 0-9.828 4.41-9.828 9.83s4.408 9.83 9.829 9.83c1.834 0 3.552-.505 5.022-1.383l5.021 5.021c2.144 2.141 5.384-1.096 3.239-3.24zm-20.064-10.228c0-3.739 3.043-6.782 6.782-6.782s6.782 3.042 6.782 6.782-3.043 6.782-6.782 6.782-6.782-3.043-6.782-6.782zm2.01-1.764c1.984-4.599 8.664-4.066 9.922.749-2.534-2.974-6.993-3.294-9.922-.749z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -286,14 +286,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-6-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-7.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<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\r\n\tc13.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\r\n\t 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\r\n\tv-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z\r\n\t 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\r\n\th22.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\r\n\tc23.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\r\n\tC341.734,61.089,312.605,49.982,283.451,50V72.682z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M5 6c-1.104 0-2 .896-2 2v14c0 1.104.896 2 2 2h8c1.104 0 2-.896 2-2v-14c0-1.104-.896-2-2-2h-8zm2 15h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm0-3h-8v-7h8v7zm0-11.688c.944-.001 1.889.359 2.608 1.08.721.72 1.082 1.664 1.082 2.606h1.554c-.001-1.341-.514-2.684-1.538-3.707-1.025-1.022-2.365-1.533-3.706-1.532v1.553zm0-2.718c1.639-.001 3.277.623 4.53 1.874 1.251 1.25 1.877 2.892 1.878 4.531h1.592c-.001-2.047-.782-4.096-2.345-5.658-1.562-1.562-3.609-2.341-5.655-2.34v1.593z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -310,14 +310,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-10-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-14.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"pen-10-icon\" d=\"M244.558,199.493l67.827,67.826l-73.17,134.531c0,0-90.805,23.4-147.694,60.027l-14.185-14.182\r\n\tl68.113-68.105c5.975-5.982,13.726-9.773,22.11-10.807c4.642-0.582,9.128-2.621,12.696-6.205c8.538-8.547,8.546-22.4-0.002-30.951\r\n\tc-8.549-8.543-22.407-8.543-30.959-0.002c-3.573,3.572-5.623,8.061-6.199,12.693c-1.028,8.371-4.834,16.15-10.8,22.117\r\n\tl-68.104,68.105L50,420.354c37.028-57.496,60.021-147.693,60.021-147.693L244.558,199.493z M315.896,50.122\r\n\tc-22.784,44.143-53.014,100-53.014,100l98.872,98.869c0,0,55.909-30.086,100.246-52.766L315.896,50.122z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.014 6.54s2.147-3.969 3.475-6.54l8.511 8.511c-2.583 1.321-6.556 3.459-6.556 3.459l-5.43-5.43zm-8.517 6.423s-1.339 5.254-3.497 8.604l.827.826 3.967-3.967c.348-.348.569-.801.629-1.288.034-.27.153-.532.361-.74.498-.498 1.306-.498 1.803 0 .498.499.498 1.305 0 1.803-.208.209-.469.328-.74.361-.488.061-.94.281-1.288.63l-3.967 3.968.826.84c3.314-2.133 8.604-3.511 8.604-3.511l4.262-7.837-3.951-3.951-7.836 4.262z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -334,14 +334,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-2-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-3.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<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\n\n\tc-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\n\n\tC154.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\n\n\t 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\n\n\tL369.496,284L351.818,301.678z\"/>\n\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M10.605 0h-10.604v10.609l13.39 13.391 10.609-10.605-13.395-13.395zm-7.019 6.414c-.781-.782-.781-2.047 0-2.828.782-.781 2.048-.781 2.828-.002.782.783.782 2.048 0 2.83-.781.781-2.046.781-2.828 0zm6.823 8.947l-4.243-4.242.708-.708 4.243 4.243-.708.707zm4.949.707l-7.07-7.071.707-.707 7.071 7.071-.708.707zm2.121-2.121l-7.071-7.071.707-.707 7.071 7.071-.707.707z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -358,14 +358,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-time-13-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-user-5.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"time-13-icon\" d=\"M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933\r\n\tc0,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\r\n\tc-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\r\n\tc-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\r\n\tc-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\r\n\tc-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\r\n\tc7.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\r\n\t 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\r\n\tH197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M19 7.001c0 3.865-3.134 7-7 7s-7-3.135-7-7c0-3.867 3.134-7.001 7-7.001s7 3.134 7 7.001zm-1.598 7.18c-1.506 1.137-3.374 1.82-5.402 1.82-2.03 0-3.899-.685-5.407-1.822-4.072 1.793-6.593 7.376-6.593 9.821h24c0-2.423-2.6-8.006-6.598-9.819z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -382,14 +382,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-6-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-8.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"warning-6-icon\" d=\"M239.939,231.352h32.121v97.421h-32.121V231.352z M256,379.019c-9.574,0-17.334-7.761-17.334-17.334\n\tc0-9.574,7.76-17.335,17.334-17.335c9.573,0,17.334,7.761,17.334,17.335C273.334,371.258,265.573,379.019,256,379.019z M256,78.07\n\tL50,434.873h412L256,78.07z M256,158.07l136.718,236.803H119.282L256,158.07z\"/>\n</svg>\n\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 5.177l8.631 15.823h-17.262l8.631-15.823zm0-4.177l-12 22h24l-12-22zm-1 9h2v6h-2v-6zm1 9.75c-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25 1.25.56 1.25 1.25-.561 1.25-1.25 1.25z\"/></svg>";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -406,38 +406,14 @@ root: root
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-wireless-6-icon.svg"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-8.svg"] = (function() {
function root(env, context, frame, runtime, cb) { function root(env, context, frame, runtime, cb) {
var lineno = null; var lineno = null;
var colno = null; var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"wireless-6-icon\" d=\"M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35\r\n\tc-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796\r\n\tc27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936\r\n\tc-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599\r\n\tc11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269\r\n\ts-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297\r\n\tc0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z\"/>\r\n</svg>\r\n"; output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 3.752l-4.423-3.752-7.771 9.039-7.647-9.008-4.159 4.278c2.285 2.885 5.284 5.903 8.362 8.708l-8.165 9.447 1.343 1.487c1.978-1.335 5.981-4.373 10.205-7.958 4.304 3.67 8.306 6.663 10.229 8.006l1.449-1.278-8.254-9.724c3.287-2.973 6.584-6.354 8.831-9.245z\"/></svg>";
if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-5-icon.svg"] = (function() {
function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
var parentTemplate = null;
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"x-mark-5-icon\" d=\"M432.546,133.462L367.133,76.39L254.078,210.715L140.967,73.702l-61.513,65.068\n\n\tc33.791,43.885,78.146,89.797,123.688,132.465L82.993,413.987l19.865,22.629c29.251-20.31,87.839-65.578,150.312-120.092\n\n\tc63.662,55.812,122.861,101.336,151.301,121.773l21.438-19.443L303.804,270.95C352.439,225.709,399.308,177.442,432.546,133.462z\"/>\n\n</svg>\n\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -461,7 +437,7 @@ var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n <title>Certidude server</title>\n <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n <nav id=\"menu\">\n <ul class=\"container\">\n <li data-section=\"requests\">Requests</li>\n <li data-section=\"signed\">Signed</li>\n <li data-section=\"revoked\">Revoked</li>\n <li data-section=\"config\">Configuration</li>\n <li data-section=\"log\">Log</li>\n </ul>\n </nav>\n <div id=\"container\" class=\"container\">\n Loading certificate authority...\n </div>\n</body>\n\n<footer>\n <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n"; output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n <title>Certidude server</title>\n <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n <nav id=\"menu\">\n <ul class=\"container\">\n <li data-section=\"about\">Profile</li>\n <li id=\"section-requests\" data-section=\"requests\" style=\"display:none;\">Requests</li>\n <li id=\"section-signed\" data-section=\"signed\" style=\"display:none;\">Signed</li>\n <li id=\"section-revoked\" data-section=\"revoked\" style=\"display:none;\">Revoked</li>\n <li id=\"section-config\" data-section=\"config\" style=\"display:none;\">Configuration</li>\n <li id=\"section-log\" data-section=\"log\" style=\"display:none;\">Log</li>\n </ul>\n </nav>\n <div id=\"container\" class=\"container\">\n Loading certificate authority...\n </div>\n</body>\n\n<footer>\n <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
@ -485,12 +461,38 @@ var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "\n<section id=\"about\">\n<p>Hi "; output += "\n<section id=\"about\">\n<h2>";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"username"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"gn"), env.opts.autoescape);
output += ",</p>\n\n<p>Request submission is allowed from: "; output += " ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets")) { output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"sn"), env.opts.autoescape);
output += " (";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"name"), env.opts.autoescape);
output += ") settings</h2>\n\n<p>Mails will be sent to: ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape);
output += "</p>\n\n<p>You can click <a href=\"/api/bundle/\">here</a> to generate bundle\nfor current user account.</p>\n\n";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
output += "\n\n<h2>Authority certificate</h2>\n\n<p>Several things such as CRL location and e-mails are hardcoded into\nthe <a href=\"/api/certificate\">certificate</a> and\nas such require complete reset of X509 infrastructure if some of them needs to be changed:</p>\n\n<p>Mails will appear from: ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"certificate")),"email_address"), env.opts.autoescape);
output += "</p>\n\n\n<h2>Authority settings</h2>\n\n<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>\n\n<p>Outgoing mail server:\n";
if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox")) {
output += "\n ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox"), env.opts.autoescape);
output += "\n";
;
}
else {
output += "\n E-mail disabled\n";
;
}
output += "</p>\n\n<p>Authenticated users allowed from:\n\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) {
output += "\n anywhere\n </p>\n";
;
}
else {
output += "\n </p>\n <ul>\n ";
frame = frame.push(); frame = frame.push();
var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets");
if(t_3) {var t_2 = t_3.length; if(t_3) {var t_2 = t_3.length;
for(var t_1=0; t_1 < t_3.length; t_1++) { for(var t_1=0; t_1 < t_3.length; t_1++) {
var t_4 = t_3[t_1]; var t_4 = t_3[t_1];
@ -502,26 +504,29 @@ frame.set("loop.revindex0", t_2 - t_1 - 1);
frame.set("loop.first", t_1 === 0); frame.set("loop.first", t_1 === 0);
frame.set("loop.last", t_1 === t_2 - 1); frame.set("loop.last", t_1 === t_2 - 1);
frame.set("loop.length", t_2); frame.set("loop.length", t_2);
output += "\n <li>";
output += runtime.suppressValue(t_4, env.opts.autoescape); output += runtime.suppressValue(t_4, env.opts.autoescape);
output += " "; output += "</li>\n ";
; ;
} }
} }
frame = frame.pop(); frame = frame.pop();
output += "\n </ul>\n";
;
}
output += "\n\n\n<p>Request submission is allowed from:\n\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) {
output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "anywhere"; output += "\n </p>\n <ul>\n ";
;
}
output += "</p>\n<p>Autosign is allowed from: ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets")) {
frame = frame.push(); frame = frame.push();
var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"); var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets");
if(t_7) {var t_6 = t_7.length; if(t_7) {var t_6 = t_7.length;
for(var t_5=0; t_5 < t_7.length; t_5++) { for(var t_5=0; t_5 < t_7.length; t_5++) {
var t_8 = t_7[t_5]; var t_8 = t_7[t_5];
frame.set("i", t_8); frame.set("subnet", t_8);
frame.set("loop.index", t_5 + 1); frame.set("loop.index", t_5 + 1);
frame.set("loop.index0", t_5); frame.set("loop.index0", t_5);
frame.set("loop.revindex", t_6 - t_5); frame.set("loop.revindex", t_6 - t_5);
@ -529,26 +534,29 @@ frame.set("loop.revindex0", t_6 - t_5 - 1);
frame.set("loop.first", t_5 === 0); frame.set("loop.first", t_5 === 0);
frame.set("loop.last", t_5 === t_6 - 1); frame.set("loop.last", t_5 === t_6 - 1);
frame.set("loop.length", t_6); frame.set("loop.length", t_6);
output += "\n <li>";
output += runtime.suppressValue(t_8, env.opts.autoescape); output += runtime.suppressValue(t_8, env.opts.autoescape);
output += " "; output += "</li>\n ";
; ;
} }
} }
frame = frame.pop(); frame = frame.pop();
output += "\n </ul>\n";
;
}
output += "\n\n<p>Autosign is allowed from:\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) {
output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "nowhere"; output += "\n </p>\n <ul>\n ";
;
}
output += "</p>\n<p>Authority administration is allowed from: ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets")) {
frame = frame.push(); frame = frame.push();
var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets");
if(t_11) {var t_10 = t_11.length; if(t_11) {var t_10 = t_11.length;
for(var t_9=0; t_9 < t_11.length; t_9++) { for(var t_9=0; t_9 < t_11.length; t_9++) {
var t_12 = t_11[t_9]; var t_12 = t_11[t_9];
frame.set("i", t_12); frame.set("subnet", t_12);
frame.set("loop.index", t_9 + 1); frame.set("loop.index", t_9 + 1);
frame.set("loop.index0", t_9); frame.set("loop.index0", t_9);
frame.set("loop.revindex", t_10 - t_9); frame.set("loop.revindex", t_10 - t_9);
@ -556,25 +564,29 @@ frame.set("loop.revindex0", t_10 - t_9 - 1);
frame.set("loop.first", t_9 === 0); frame.set("loop.first", t_9 === 0);
frame.set("loop.last", t_9 === t_10 - 1); frame.set("loop.last", t_9 === t_10 - 1);
frame.set("loop.length", t_10); frame.set("loop.length", t_10);
output += "\n <li>";
output += runtime.suppressValue(t_12, env.opts.autoescape); output += runtime.suppressValue(t_12, env.opts.autoescape);
output += " "; output += "</li>\n ";
; ;
} }
} }
frame = frame.pop(); frame = frame.pop();
output += "\n </ul>\n";
;
}
output += "\n\n<p>Authority administration is allowed from:\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) {
output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "anywhere"; output += "\n <ul>\n ";
;
}
output += "\n<p>Authority administration allowed for: ";
frame = frame.push(); frame = frame.push();
var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets");
if(t_15) {var t_14 = t_15.length; if(t_15) {var t_14 = t_15.length;
for(var t_13=0; t_13 < t_15.length; t_13++) { for(var t_13=0; t_13 < t_15.length; t_13++) {
var t_16 = t_15[t_13]; var t_16 = t_15[t_13];
frame.set("i", t_16); frame.set("subnet", t_16);
frame.set("loop.index", t_13 + 1); frame.set("loop.index", t_13 + 1);
frame.set("loop.index0", t_13); frame.set("loop.index0", t_13);
frame.set("loop.revindex", t_14 - t_13); frame.set("loop.revindex", t_14 - t_13);
@ -582,71 +594,132 @@ frame.set("loop.revindex0", t_14 - t_13 - 1);
frame.set("loop.first", t_13 === 0); frame.set("loop.first", t_13 === 0);
frame.set("loop.last", t_13 === t_14 - 1); frame.set("loop.last", t_13 === t_14 - 1);
frame.set("loop.length", t_14); frame.set("loop.length", t_14);
output += "\n <li>";
output += runtime.suppressValue(t_16, env.opts.autoescape); output += runtime.suppressValue(t_16, env.opts.autoescape);
output += " "; output += "</li>\n ";
; ;
} }
} }
frame = frame.pop(); frame = frame.pop();
output += "</p>\n</section>\n"; output += "\n </ul>\n";
var t_17; ;
t_17 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
frame.set("s", t_17, true);
if(frame.topLevel) {
context.setVariable("s", t_17);
} }
if(frame.topLevel) { output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n";
context.addExport("s", t_17);
}
output += "\n\n\n<section id=\"requests\">\n <h1>Pending requests</h1>\n\n\n <ul id=\"pending_requests\">\n ";
frame = frame.push(); frame = frame.push();
var t_20 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"requests"); var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
runtime.asyncEach(t_20, 1, function(request, t_18, t_19,next) { if(t_19) {var t_17;
frame.set("request", request); if(runtime.isArray(t_19)) {
frame.set("loop.index", t_18 + 1); var t_18 = t_19.length;
frame.set("loop.index0", t_18); for(t_17=0; t_17 < t_19.length; t_17++) {
frame.set("loop.revindex", t_19 - t_18); var t_20 = t_19[t_17][0]
frame.set("loop.revindex0", t_19 - t_18 - 1); frame.set("handle", t_19[t_17][0]);
frame.set("loop.first", t_18 === 0); var t_21 = t_19[t_17][1]
frame.set("loop.last", t_18 === t_19 - 1); frame.set("full_name", t_19[t_17][1]);
frame.set("loop.length", t_19); frame.set("loop.index", t_17 + 1);
output += "\n "; frame.set("loop.index0", t_17);
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_23,t_21) { frame.set("loop.revindex", t_18 - t_17);
if(t_23) { cb(t_23); return; } frame.set("loop.revindex0", t_18 - t_17 - 1);
t_21.render(context.getVariables(), frame, function(t_24,t_22) { frame.set("loop.first", t_17 === 0);
if(t_24) { cb(t_24); return; } frame.set("loop.last", t_17 === t_18 - 1);
output += t_22 frame.set("loop.length", t_18);
output += "\n\t "; output += "\n <li>";
next(t_18); output += runtime.suppressValue(t_21, env.opts.autoescape);
})}); output += "</li>\n";
}, function(t_26,t_25) { ;
if(t_26) { cb(t_26); return; } }
} else {
t_17 = -1;
var t_18 = runtime.keys(t_19).length;
for(var t_22 in t_19) {
t_17++;
var t_23 = t_19[t_22];
frame.set("handle", t_22);
frame.set("full_name", t_23);
frame.set("loop.index", t_17 + 1);
frame.set("loop.index0", t_17);
frame.set("loop.revindex", t_18 - t_17);
frame.set("loop.revindex0", t_18 - t_17 - 1);
frame.set("loop.first", t_17 === 0);
frame.set("loop.last", t_17 === t_18 - 1);
frame.set("loop.length", t_18);
output += "\n <li>";
output += runtime.suppressValue(t_23, env.opts.autoescape);
output += "</li>\n";
;
}
}
}
frame = frame.pop(); frame = frame.pop();
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign! You can submit a certificate signing request by:</p>\n <pre>certidude setup client "; output += "\n</ul>\n</section>\n\n";
;
}
else {
output += "\n<p>Here you can renew your certificates</p>\n\n";
;
}
output += "\n\n";
var t_24;
t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
frame.set("s", t_24, true);
if(frame.topLevel) {
context.setVariable("s", t_24);
}
if(frame.topLevel) {
context.addExport("s", t_24);
}
output += "\n\n\n";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
output += "\n<section id=\"requests\">\n <h1>Pending requests</h1>\n\n <p>Submit a certificate signing request with Certidude:</p>\n <pre>certidude setup client ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
output += "</pre>\n </li>\n </ul>\n</section>\n\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\n "; output += "</pre>\n\n <ul id=\"pending_requests\">\n ";
frame = frame.push(); frame = frame.push();
var t_29 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"signed"))); var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
runtime.asyncEach(t_29, 1, function(certificate, t_27, t_28,next) { if(t_27) {var t_26 = t_27.length;
frame.set("certificate", certificate); for(var t_25=0; t_25 < t_27.length; t_25++) {
frame.set("loop.index", t_27 + 1); var t_28 = t_27[t_25];
frame.set("loop.index0", t_27); frame.set("request", t_28);
frame.set("loop.revindex", t_28 - t_27); frame.set("loop.index", t_25 + 1);
frame.set("loop.revindex0", t_28 - t_27 - 1); frame.set("loop.index0", t_25);
frame.set("loop.first", t_27 === 0); frame.set("loop.revindex", t_26 - t_25);
frame.set("loop.last", t_27 === t_28 - 1); frame.set("loop.revindex0", t_26 - t_25 - 1);
frame.set("loop.length", t_28); frame.set("loop.first", t_25 === 0);
output += "\n "; frame.set("loop.last", t_25 === t_26 - 1);
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_32,t_30) { frame.set("loop.length", t_26);
output += "\n ";
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) {
if(t_31) { cb(t_31); return; }
t_29.render(context.getVariables(), frame, function(t_32,t_30) {
if(t_32) { cb(t_32); return; } if(t_32) { cb(t_32); return; }
t_30.render(context.getVariables(), frame, function(t_33,t_31) { output += t_30
if(t_33) { cb(t_33); return; }
output += t_31
output += "\n\t "; output += "\n\t ";
next(t_27);
})}); })});
}, function(t_35,t_34) { }
if(t_35) { cb(t_35); return; } }
frame = frame.pop();
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign!</p>\n </li>\n </ul>\n</section>\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\n ";
frame = frame.push();
var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
if(t_35) {var t_34 = t_35.length;
for(var t_33=0; t_33 < t_35.length; t_33++) {
var t_36 = t_35[t_33];
frame.set("certificate", t_36);
frame.set("loop.index", t_33 + 1);
frame.set("loop.index0", t_33);
frame.set("loop.revindex", t_34 - t_33);
frame.set("loop.revindex0", t_34 - t_33 - 1);
frame.set("loop.first", t_33 === 0);
frame.set("loop.last", t_33 === t_34 - 1);
frame.set("loop.length", t_34);
output += "\n ";
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) {
if(t_39) { cb(t_39); return; }
t_37.render(context.getVariables(), frame, function(t_40,t_38) {
if(t_40) { cb(t_40); return; }
output += t_38
output += "\n\t ";
})});
}
}
frame = frame.pop(); frame = frame.pop();
output += "\n </ul>\n</section>\n\n<section id=\"log\">\n <h1>Log</h1>\n <p>\n <input id=\"log_level_critical\" type=\"checkbox\" checked/> <label for=\"log_level_critical\">Critical</label>\n <input id=\"log_level_error\" type=\"checkbox\" checked/> <label for=\"log_level_error\">Errors</label>\n <input id=\"log_level_warning\" type=\"checkbox\" checked/> <label for=\"log_level_warning\">Warnings</label>\n <input id=\"log_level_info\" type=\"checkbox\" checked/> <label for=\"log_level_info\">Info</label>\n <input id=\"log_level_debug\" type=\"checkbox\"/> <label for=\"log_level_debug\">Debug</label>\n </p>\n <ul id=\"log_entries\">\n </ul>\n</section>\n\n<section id=\"revoked\">\n <h1>Revoked certificates</h1>\n <p>To fetch certificate revocation list:</p>\n <pre>curl "; output += "\n </ul>\n</section>\n\n<section id=\"log\">\n <h1>Log</h1>\n <p>\n <input id=\"log_level_critical\" type=\"checkbox\" checked/> <label for=\"log_level_critical\">Critical</label>\n <input id=\"log_level_error\" type=\"checkbox\" checked/> <label for=\"log_level_error\">Errors</label>\n <input id=\"log_level_warning\" type=\"checkbox\" checked/> <label for=\"log_level_warning\">Warnings</label>\n <input id=\"log_level_info\" type=\"checkbox\" checked/> <label for=\"log_level_info\">Info</label>\n <input id=\"log_level_debug\" type=\"checkbox\"/> <label for=\"log_level_debug\">Debug</label>\n </p>\n <ul id=\"log_entries\">\n </ul>\n</section>\n\n<section id=\"revoked\">\n <h1>Revoked certificates</h1>\n <p>To fetch certificate revocation list:</p>\n <pre>curl ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape);
@ -656,41 +729,44 @@ output += "/certificate/ > session.pem\n openssl ocsp -issuer session.pem -CA
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n "; output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n ";
frame = frame.push(); frame = frame.push();
var t_38 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"revoked"); var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
if(t_38) {var t_37 = t_38.length; if(t_43) {var t_42 = t_43.length;
for(var t_36=0; t_36 < t_38.length; t_36++) { for(var t_41=0; t_41 < t_43.length; t_41++) {
var t_39 = t_38[t_36]; var t_44 = t_43[t_41];
frame.set("j", t_39); frame.set("j", t_44);
frame.set("loop.index", t_36 + 1); frame.set("loop.index", t_41 + 1);
frame.set("loop.index0", t_36); frame.set("loop.index0", t_41);
frame.set("loop.revindex", t_37 - t_36); frame.set("loop.revindex", t_42 - t_41);
frame.set("loop.revindex0", t_37 - t_36 - 1); frame.set("loop.revindex0", t_42 - t_41 - 1);
frame.set("loop.first", t_36 === 0); frame.set("loop.first", t_41 === 0);
frame.set("loop.last", t_36 === t_37 - 1); frame.set("loop.last", t_41 === t_42 - 1);
frame.set("loop.length", t_37); frame.set("loop.length", t_42);
output += "\n <li id=\"certificate_"; output += "\n <li id=\"certificate_";
output += runtime.suppressValue(runtime.memberLookup((t_39),"sha256sum"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape);
output += "\">\n "; output += "\">\n ";
output += runtime.suppressValue(runtime.memberLookup((t_39),"changed"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape);
output += "\n "; output += "\n ";
output += runtime.suppressValue(runtime.memberLookup((t_39),"serial_number"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape);
output += " <span class=\"monospace\">"; output += " <span class=\"monospace\">";
output += runtime.suppressValue(runtime.memberLookup((t_39),"identity"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape);
output += "</span>\n </li>\n "; output += "</span>\n </li>\n ";
; ;
} }
} }
if (!t_37) { if (!t_42) {
output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t "; output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
} }
frame = frame.pop(); frame = frame.pop();
output += "\n </ul>\n</section>\n\n<section id=\"config\">\n</section>\n"; output += "\n </ul>\n</section>\n\n<section id=\"config\">\n</section>\n\n";
;
}
output += "\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
cb(null, output); cb(null, output);
} }
})}); ;
} catch (e) { } catch (e) {
cb(runtime.handleError(e, lineno, colno)); cb(runtime.handleError(e, lineno, colno));
} }
@ -894,8 +970,8 @@ var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<li id=\"request_"; output += "<li id=\"request-";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"),"@","--"),".","-"), env.opts.autoescape);
output += "\" class=\"filterable\">\n\n<a class=\"button icon download\" href=\"/api/request/"; output += "\" class=\"filterable\">\n\n<a class=\"button icon download\" href=\"/api/request/";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
output += "/\">Fetch</a>\n"; output += "/\">Fetch</a>\n";
@ -912,7 +988,7 @@ output += "\n<button title=\"Please use certidude command-line utility to sign u
output += "\n<button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/request/"; output += "\n<button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/request/";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
output += "/',type:'delete'});\">Delete</button>\n\n\n<div class=\"monospace\">\n"; output += "/',type:'delete'});\">Delete</button>\n\n\n<div class=\"monospace\">\n";
env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/request.html", null, function(t_3,t_1) { env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/request.html", null, function(t_3,t_1) {
if(t_3) { cb(t_3); return; } if(t_3) { cb(t_3); return; }
t_1.render(context.getVariables(), frame, function(t_4,t_2) { t_1.render(context.getVariables(), frame, function(t_4,t_2) {
if(t_4) { cb(t_4); return; } if(t_4) { cb(t_4); return; }
@ -920,9 +996,9 @@ output += t_2
output += "\n"; output += "\n";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape);
output += "\n</div>\n\n"; output += "\n</div>\n\n";
(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) { if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) {
output += "\n<div class=\"email\">"; output += "\n<div class=\"email\">";
env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/request.html", null, function(t_7,t_5) { env.getTemplate("img/iconmonstr-email-2.svg", false, "views/request.html", null, function(t_7,t_5) {
if(t_7) { cb(t_7); return; } if(t_7) { cb(t_7); return; }
t_5.render(context.getVariables(), frame, function(t_8,t_6) { t_5.render(context.getVariables(), frame, function(t_8,t_6) {
if(t_8) { cb(t_8); return; } if(t_8) { cb(t_8); return; }
@ -930,12 +1006,10 @@ output += t_6
output += " "; output += " ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape);
output += "</div>\n"; output += "</div>\n";
cb()})}); })});
} }
else { output += "\n\n<div class=\"monospace\">\n";
cb()} env.getTemplate("img/iconmonstr-key-3.svg", false, "views/request.html", null, function(t_11,t_9) {
})(function() {output += "\n\n<div class=\"monospace\">\n";
env.getTemplate("img/iconmonstr-key-2-icon.svg", false, "views/request.html", null, function(t_11,t_9) {
if(t_11) { cb(t_11); return; } if(t_11) { cb(t_11); return; }
t_9.render(context.getVariables(), frame, function(t_12,t_10) { t_9.render(context.getVariables(), frame, function(t_12,t_10) {
if(t_12) { cb(t_12); return; } if(t_12) { cb(t_12); return; }
@ -957,9 +1031,9 @@ if(frame.topLevel) {
context.addExport("key_usage", t_13); context.addExport("key_usage", t_13);
} }
output += "\n"; output += "\n";
(function(cb) {if(runtime.contextOrFrameLookup(context, frame, "key_usage")) { if(runtime.contextOrFrameLookup(context, frame, "key_usage")) {
output += "\n<div>\n"; output += "\n<div>\n";
env.getTemplate("img/iconmonstr-flag-3-icon.svg", false, "views/request.html", null, function(t_16,t_14) { env.getTemplate("img/iconmonstr-flag-3.svg", false, "views/request.html", null, function(t_16,t_14) {
if(t_16) { cb(t_16); return; } if(t_16) { cb(t_16); return; }
t_14.render(context.getVariables(), frame, function(t_17,t_15) { t_14.render(context.getVariables(), frame, function(t_17,t_15) {
if(t_17) { cb(t_17); return; } if(t_17) { cb(t_17); return; }
@ -967,17 +1041,15 @@ output += t_15
output += "\n"; output += "\n";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape);
output += "\n</div>\n"; output += "\n</div>\n";
cb()})}); })});
} }
else { output += "\n\n</li>\n\n";
cb()}
})(function() {output += "\n\n</li>\n\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
cb(null, output); cb(null, output);
} }
})})})})})}); })})})});
} catch (e) { } catch (e) {
cb(runtime.handleError(e, lineno, colno)); cb(runtime.handleError(e, lineno, colno));
} }
@ -995,8 +1067,8 @@ var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<li id=\"certificate_"; output += "<li id=\"certificate-";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"sha256sum"), env.opts.autoescape); output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"),"@","--"),".","-"), env.opts.autoescape);
output += "\" data-dn=\""; output += "\" data-dn=\"";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape);
output += "\" data-cn=\""; output += "\" data-cn=\"";
@ -1006,17 +1078,17 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
output += "/\">Fetch</a>\n <button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/"; output += "/\">Fetch</a>\n <button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
output += "/',type:'delete'});\">Revoke</button>\n\n <div class=\"monospace\">\n "; output += "/',type:'delete'});\">Revoke</button>\n\n <div class=\"monospace\">\n ";
env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/signed.html", null, function(t_3,t_1) { env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/signed.html", null, function(t_3,t_1) {
if(t_3) { cb(t_3); return; } if(t_3) { cb(t_3); return; }
t_1.render(context.getVariables(), frame, function(t_4,t_2) { t_1.render(context.getVariables(), frame, function(t_4,t_2) {
if(t_4) { cb(t_4); return; } if(t_4) { cb(t_4); return; }
output += t_2 output += t_2
output += "\n "; output += "\n ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
output += "\n </div>\n\n "; output += "\n </div>\n\n ";
(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) { if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) {
output += "\n <div class=\"email\">"; output += "\n <div class=\"email\">";
env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/signed.html", null, function(t_7,t_5) { env.getTemplate("img/iconmonstr-email-2.svg", false, "views/signed.html", null, function(t_7,t_5) {
if(t_7) { cb(t_7); return; } if(t_7) { cb(t_7); return; }
t_5.render(context.getVariables(), frame, function(t_8,t_6) { t_5.render(context.getVariables(), frame, function(t_8,t_6) {
if(t_8) { cb(t_8); return; } if(t_8) { cb(t_8); return; }
@ -1024,32 +1096,53 @@ output += t_6
output += " "; output += " ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape);
output += "</div>\n "; output += "</div>\n ";
cb()})}); })});
} }
else { output += "\n \n ";
cb()} if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
})(function() {output += "\n\n "; output += "\n <div class=\"person\">";
output += "\n\n <div class=\"tags\">\n <select class=\"icon tag\" data-cn=\""; env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
output += "\" onChange=\"onNewTagClicked();\">\n <option value=\"\">Add tag...</option>\n ";
env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_11,t_9) {
if(t_11) { cb(t_11); return; } if(t_11) { cb(t_11); return; }
t_9.render(context.getVariables(), frame, function(t_12,t_10) { t_9.render(context.getVariables(), frame, function(t_12,t_10) {
if(t_12) { cb(t_12); return; } if(t_12) { cb(t_12); return; }
output += t_10 output += t_10
output += "\n </select>\n </div>\n\n <div class=\"status\">\n "; output += " ";
env.getTemplate("views/status.html", false, "views/signed.html", null, function(t_15,t_13) { output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name"), env.opts.autoescape);
output += " ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname"), env.opts.autoescape);
output += "</div>\n ";
})});
}
output += "\n\n <div class=\"lifetime\" title=\"Valid from ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape);
output += " to ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape);
output += "\">\n ";
env.getTemplate("img/iconmonstr-calendar-6.svg", false, "views/signed.html", null, function(t_15,t_13) {
if(t_15) { cb(t_15); return; } if(t_15) { cb(t_15); return; }
t_13.render(context.getVariables(), frame, function(t_16,t_14) { t_13.render(context.getVariables(), frame, function(t_16,t_14) {
if(t_16) { cb(t_16); return; } if(t_16) { cb(t_16); return; }
output += t_14 output += t_14
output += "\n </div>\n</li>\n"; output += "\n <time>";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape);
output += "</time> -\n <time>";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape);
output += "</time>\n </div>\n\n ";
output += "\n\n <div class=\"tags\">\n <select class=\"icon tag\" data-cn=\"";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
output += "\" onChange=\"onNewTagClicked();\">\n <option value=\"\">Add tag...</option>\n ";
env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_19,t_17) {
if(t_19) { cb(t_19); return; }
t_17.render(context.getVariables(), frame, function(t_20,t_18) {
if(t_20) { cb(t_20); return; }
output += t_18
output += "\n </select>\n </div>\n <div class=\"status\"></div>\n</li>\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {
cb(null, output); cb(null, output);
} }
})})})})})})}); })})})})})});
} catch (e) { } catch (e) {
cb(runtime.handleError(e, lineno, colno)); cb(runtime.handleError(e, lineno, colno));
} }
@ -1135,6 +1228,44 @@ return {
root: root root: root
}; };
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tags.html"] = (function() {
function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
var parentTemplate = null;
output += "<span id=\"tag_";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape);
output += "\" onclick=\"onTagClicked()\"\ntitle=\"";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape);
output += "=";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape);
output += "\" class=\"";
output += runtime.suppressValue(env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"),"."," "), env.opts.autoescape);
output += "\"\ndata-id=\"";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape);
output += "\" data-key=\"";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape);
output += "\">";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape);
output += "</span>\n";
if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})(); })();
})(); })();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() { (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() {
@ -1144,7 +1275,7 @@ var colno = null;
var output = ""; var output = "";
try { try {
var parentTemplate = null; var parentTemplate = null;
output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channela\">5GHz channel number</option>\n<option value=\"wireless.channelb\">2.4GHz channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n"; output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channel\">Channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n";
if(parentTemplate) { if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else { } else {

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -1,36 +1,127 @@
<section id="about"> <section id="about">
<p>Hi {{session.username}},</p> <h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
<p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p> <p>Mails will be sent to: {{ session.user.mail }}</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>You can click <a href="/api/bundle/">here</a> to generate bundle
<p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p> for current user account.</p>
{% if session.authority %}
<h2>Authority certificate</h2>
<p>Several things such as CRL location and e-mails are hardcoded into
the <a href="/api/certificate">certificate</a> and
as such require complete reset of X509 infrastructure if some of them needs to be changed:</p>
<p>Mails will appear from: {{ session.authority.certificate.email_address }}</p>
<h2>Authority settings</h2>
<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>
<p>Outgoing mail server:
{% if session.authority.outbox %}
{{ session.authority.outbox }}
{% else %}
E-mail disabled
{% endif %}</p>
<p>Authenticated users allowed from:
{% if "0.0.0.0/0" in session.user_subnets %}
anywhere
</p>
{% else %}
</p>
<ul>
{% for i in session.user_subnets %}
<li>{{ i }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Request submission is allowed from:
{% if "0.0.0.0/0" in session.request_subnets %}
anywhere
</p>
{% else %}
</p>
<ul>
{% for subnet in session.request_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Autosign is allowed from:
{% if "0.0.0.0/0" in session.autosign_subnets %}
anywhere
</p>
{% else %}
</p>
<ul>
{% for subnet in session.autosign_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Authority administration is allowed from:
{% if "0.0.0.0/0" in session.admin_subnets %}
anywhere
</p>
{% else %}
<ul>
{% for subnet in session.admin_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Authority administration allowed for:</p>
<ul>
{% for handle, full_name in session.admin_users %}
<li>{{ full_name }}</li>
{% endfor %}
</ul>
</section> </section>
{% else %}
<p>Here you can renew your certificates</p>
{% endif %}
{% set s = session.certificate.identity %} {% set s = session.certificate.identity %}
{% if session.authority %}
<section id="requests"> <section id="requests">
<h1>Pending requests</h1> <h1>Pending requests</h1>
<p>Submit a certificate signing request with Certidude:</p>
<pre>certidude setup client {{session.common_name}}</pre>
<ul id="pending_requests"> <ul id="pending_requests">
{% for request in session.requests %} {% for request in session.authority.requests %}
{% include "views/request.html" %} {% include "views/request.html" %}
{% endfor %} {% endfor %}
<li class="notify"> <li class="notify">
<p>No certificate signing requests to sign! You can submit a certificate signing request by:</p> <p>No certificate signing requests to sign!</p>
<pre>certidude setup client {{session.common_name}}</pre>
</li> </li>
</ul> </ul>
</section> </section>
<section id="signed"> <section id="signed">
<h1>Signed certificates</h1> <h1>Signed certificates</h1>
<input id="search" type="search" class="icon search"> <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.authority.signed | sort | reverse %}
{% include "views/signed.html" %} {% include "views/signed.html" %}
{% endfor %} {% endfor %}
</ul> </ul>
@ -62,7 +153,7 @@
</pre> </pre>
--> -->
<ul> <ul>
{% for j in session.revoked %} {% for j in session.authority.revoked %}
<li id="certificate_{{ j.sha256sum }}"> <li id="certificate_{{ j.sha256sum }}">
{{j.changed}} {{j.changed}}
{{j.serial_number}} <span class="monospace">{{j.identity}}</span> {{j.serial_number}} <span class="monospace">{{j.identity}}</span>
@ -75,3 +166,5 @@
<section id="config"> <section id="config">
</section> </section>
{% endif %}

View File

@ -1,4 +1,4 @@
<li id="request_{{ request.common_name }}" class="filterable"> <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
{% if request.signable %} {% if request.signable %}
@ -10,16 +10,16 @@
<div class="monospace"> <div class="monospace">
{% include 'img/iconmonstr-certificate-15-icon.svg' %} {% include 'img/iconmonstr-certificate-15.svg' %}
{{request.identity}} {{request.identity}}
</div> </div>
{% if request.email_address %} {% if request.email_address %}
<div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ request.email_address }}</div> <div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ request.email_address }}</div>
{% endif %} {% endif %}
<div class="monospace"> <div class="monospace">
{% include 'img/iconmonstr-key-2-icon.svg' %} {% include 'img/iconmonstr-key-3.svg' %}
<span title="SHA-1 of public key"> <span title="SHA-1 of public key">
{{ request.sha256sum }} {{ request.sha256sum }}
</span> </span>
@ -30,7 +30,7 @@
{% set key_usage = request.key_usage %} {% set key_usage = request.key_usage %}
{% if key_usage %} {% if key_usage %}
<div> <div>
{% include 'img/iconmonstr-flag-3-icon.svg' %} {% include 'img/iconmonstr-flag-3.svg' %}
{{request.key_usage}} {{request.key_usage}}
</div> </div>
{% endif %} {% endif %}

View File

@ -1,20 +1,30 @@
<li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable"> <li id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" 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>
<div class="monospace"> <div class="monospace">
{% include 'img/iconmonstr-certificate-15-icon.svg' %} {% include 'img/iconmonstr-certificate-15.svg' %}
{{certificate.identity}} {{certificate.common_name}}
</div> </div>
{% if certificate.email_address %} {% if certificate.email_address %}
<div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ certificate.email_address }}</div> <div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ certificate.email_address }}</div>
{% endif %} {% endif %}
{% if certificate.given_name or certificate.surname %}
<div class="person">{% include 'img/iconmonstr-user-5.svg' %} {{ certificate.given_name }} {{ certificate.surname }}</div>
{% endif %}
<div class="lifetime" title="Valid from {{ certificate.signed }} to {{ certificate.expires }}">
{% include 'img/iconmonstr-calendar-6.svg' %}
<time>{{ certificate.signed }}</time> -
<time>{{ certificate.expires }}</time>
</div>
{# {#
<div class="monospace"> <div class="monospace">
{% include 'img/iconmonstr-key-2-icon.svg' %} {% include 'img/iconmonstr-key-3.svg' %}
<span title="SHA-256 of public key"> <span title="SHA-256 of public key">
{{ certificate.sha256sum }} {{ certificate.sha256sum }}
</span> </span>
@ -23,20 +33,17 @@
</div> </div>
<div> <div>
{% include 'img/iconmonstr-flag-3-icon.svg' %} {% include 'img/iconmonstr-flag-3.svg' %}
{{certificate.key_usage}} {{certificate.key_usage}}
</div> </div>
#} #}
<div class="tags"> <div class="tags">
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();"> <select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();">
<option value="">Add tag...</option> <option value="">Add tag...</option>
{% include 'views/tagtypes.html' %} {% include 'views/tagtypes.html' %}
</select> </select>
</div>
<div class="status">
{% include 'views/status.html' %}
</div> </div>
<div class="status"></div>
</li> </li>

View File

@ -0,0 +1,3 @@
<span id="tag_{{ tag.id }}" onclick="onTagClicked()"
title="{{ tag.key }}={{ tag.value }}" class="{{ tag.key | replace('.', ' ') }}"
data-id="{{ tag.id }}" data-key="{{ tag.key }}">{{ tag.value }}</span>

View File

@ -6,6 +6,5 @@
<option value="wireless.protected.password">Protected wireless network password</option> <option value="wireless.protected.password">Protected wireless network password</option>
<option value="wireless.protected.name">Protected wireless network name</option> <option value="wireless.protected.name">Protected wireless network name</option>
<option value="wireless.public.name">Public wireless network name</option> <option value="wireless.public.name">Public wireless network name</option>
<option value="wireless.channela">5GHz channel number</option> <option value="wireless.channel">Channel number</option>
<option value="wireless.channelb">2.4GHz channel number</option>
<option value="usb.approved">Approved USB device</option> <option value="usb.approved">Approved USB device</option>

View File

@ -1,20 +1,60 @@
[authentication]
backends = pam
#backends = kerberos
#backends = ldap
#backends = kerberos ldap
#backends = kerberos pam
[accounts]
backend = posix
#backend = ldap
[authorization] [authorization]
admin_users = administrator backend = posix
admin_subnets = 0.0.0.0/0 #backend = ldap
request_subnets = 0.0.0.0/0 whitelist admin users = root administrator
autosign_subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ldap gssapi credential cache = /run/certidude/krb5cc
ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s))
ldap user filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s))
ldap admins filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=koodur,dc=com)(samaccountname=%s))
ldap member of filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s)(memberOf=%s))
ldap members filter = (&(objectclass=group)(cn=%s)(member=%s))
ldap group filter = (&(objectClass=group)(cn=%s)(member=%s))
ldap user group =
ldap admin group = domain admins
posix user group =
posix admin group = certidude
user subnets = 0.0.0.0/0
admin subnets = 0.0.0.0/0
request subnets = 0.0.0.0/0
autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
[logging]
backend = sql
database = sqlite://{{ directory }}/db.sqlite
[tagging]
backend = sql
database = sqlite://{{ directory }}/db.sqlite
[leases]
backend = sql
schema = strongswan
database = sqlite://{{ directory }}/db.sqlite
[signature] [signature]
certificate_lifetime = 1825 certificate lifetime = 1825
revocation_list_lifetime = 1 revocation list lifetime = 1
[push] [push]
server = server =
[authority] [authority]
private_key_path = {{ ca_key }} private key path = {{ ca_key }}
certificate_path = {{ ca_crt }} certificate path = {{ ca_crt }}
requests_dir = {{ directory }}/requests/ requests dir = {{ directory }}/requests/
signed_dir = {{ directory }}/signed/ signed dir = {{ directory }}/signed/
revoked_dir = {{ directory }}/revoked/ revoked dir = {{ directory }}/revoked/
outbox = smtp://localhost

View File

@ -0,0 +1,7 @@
Certificate {{certificate.common_name}} ({{certificate.serial_number}}) signed
This is simply to notify that certificate {{ certificate.common_name }}
was signed{% if signer %} by {{ signer }}{% endif %}.
Any existing certificates with the same common name were rejected by doing so
and services making use of those certificates might become unavailable.

View File

@ -0,0 +1,20 @@
server {
listen 80;
server_name {{constants.FQDN}};
rewrite ^ https://{{constants.FQDN}}$request_uri?;
}
server {
root /var/www/html;
add_header X-Frame-Options "DENY";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
listen 443 ssl;
server_name {{constants.FQDN}};
client_max_body_size 10G;
ssl_certificate {{certificate_path}};
ssl_certificate_key {{key_path}};
ssl_client_certificate {{authority_path}};
ssl_verify_client {{verify_client}};
}

View File

@ -0,0 +1,6 @@
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_dhparam {{dhparam_path}};

View File

@ -34,15 +34,21 @@ http {
} }
{% if not push_server %} {% if not push_server %}
location ~ /publish/(.*) { location /pub {
allow 127.0.0.1; allow 127.0.0.1;
push_stream_publisher admin; nchan_publisher http;
push_stream_channels_path $1; nchan_store_messages off;
nchan_channel_id $arg_id;
} }
location ~ /subscribe/(.*) { location ~ "^/lp/(.*)" {
push_stream_channels_path $1; nchan_subscriber longpoll;
push_stream_subscriber long-polling; nchan_channel_id $1;
}
location ~ "^/ev/(.*)" {
nchan_subscriber eventsource;
nchan_channel_id $1;
} }
{% endif %} {% endif %}

View File

@ -1,5 +1,5 @@
[uwsgi] [uwsgi]
exec-as-root = /usr/local/bin/certidude spawn exec-as-root = /usr/local/bin/certidude signer spawn -k
master = true master = true
processes = 1 processes = 1
vacuum = true vacuum = true
@ -15,3 +15,5 @@ buffer-size = 32768
env = LANG=C.UTF-8 env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8 env = LC_ALL=C.UTF-8
env = KRB5_KTNAME={{kerberos_keytab}} env = KRB5_KTNAME={{kerberos_keytab}}
env = KRB5CCNAME=/run/certidude/krb5cc

View File

@ -3,10 +3,9 @@ import hashlib
import re import re
import click import click
import io import io
from Crypto.Util import asn1 from certidude import constants
from OpenSSL import crypto from OpenSSL import crypto
from datetime import datetime from datetime import datetime
from certidude.signer import raw_sign, EXTENSION_WHITELIST
def subject2dn(subject): def subject2dn(subject):
bits = [] bits = []
@ -16,6 +15,10 @@ def subject2dn(subject):
return ", ".join(bits) return ", ".join(bits)
class CertificateBase: class CertificateBase:
# Others will cause browsers to import the cert instead of offering to
# download it
content_type = "application/x-pem-file"
def __repr__(self): def __repr__(self):
return self.buf return self.buf
@ -41,7 +44,7 @@ class CertificateBase:
@common_name.setter @common_name.setter
def common_name(self, value): def common_name(self, value):
return setattr(self._obj.get_subject(), "CN", value) self.subject.CN = value
@property @property
def country_code(self): def country_code(self):
@ -130,7 +133,6 @@ class CertificateBase:
def set_extensions(self, extensions): def set_extensions(self, extensions):
# X509Req().add_extensions() first invocation takes only effect?! # X509Req().add_extensions() first invocation takes only effect?!
assert self._obj.get_extensions() == [], "Extensions already set!" assert self._obj.get_extensions() == [], "Extensions already set!"
self._obj.add_extensions([ self._obj.add_extensions([
crypto.X509Extension( crypto.X509Extension(
key.encode("ascii"), key.encode("ascii"),
@ -164,6 +166,7 @@ class CertificateBase:
@property @property
def pubkey(self): def pubkey(self):
from Crypto.Util import asn1
pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey()) pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
pubkey_der=asn1.DerSequence() pubkey_der=asn1.DerSequence()
pubkey_der.decode(pubkey_asn1) pubkey_der.decode(pubkey_asn1)
@ -194,6 +197,11 @@ class CertificateBase:
class Request(CertificateBase): class Request(CertificateBase):
@property
def suggested_filename(self):
return self.common_name + ".csr"
def __init__(self, mixed=None): def __init__(self, mixed=None):
self.buf = None self.buf = None
self.path = NotImplemented self.path = NotImplemented
@ -204,27 +212,23 @@ class Request(CertificateBase):
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
self.created = datetime.fromtimestamp(mtime) self.created = datetime.fromtimestamp(mtime)
mixed = mixed.read() mixed = mixed.read()
if isinstance(mixed, bytes):
mixed = mixed.decode("ascii")
if isinstance(mixed, str): if isinstance(mixed, str):
try: try:
self.buf = mixed self.buf = mixed
mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed) mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed)
except crypto.Error: except crypto.Error:
print("Failed to parse:", mixed) raise ValueError("Failed to parse: %s" % mixed)
raise
if isinstance(mixed, crypto.X509Req): if isinstance(mixed, crypto.X509Req):
self._obj = mixed self._obj = mixed
else: else:
raise ValueError("Can't parse %s as X.509 certificate signing request!" % mixed) raise ValueError("Can't parse %s (%s) as X.509 certificate signing request!" % (mixed, type(mixed)))
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump())) assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
@property @property
def signable(self): def signable(self):
for key, value, data in self.extensions: for key, value, data in self.extensions:
if key not in EXTENSION_WHITELIST: if key not in constants.EXTENSION_WHITELIST:
return False return False
return True return True
@ -243,6 +247,11 @@ class Request(CertificateBase):
class Certificate(CertificateBase): class Certificate(CertificateBase):
@property
def suggested_filename(self):
return self.common_name + ".crt"
def __init__(self, mixed): def __init__(self, mixed):
self.buf = NotImplemented self.buf = NotImplemented
self.path = NotImplemented self.path = NotImplemented
@ -253,15 +262,12 @@ class Certificate(CertificateBase):
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
self.changed = datetime.fromtimestamp(mtime) self.changed = datetime.fromtimestamp(mtime)
mixed = mixed.read() mixed = mixed.read()
if isinstance(mixed, str): if isinstance(mixed, str):
try: try:
self.buf = mixed self.buf = mixed
mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed) mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed)
except crypto.Error: except crypto.Error:
print("Failed to parse:", mixed) raise ValueError("Failed to parse: %s" % mixed)
raise
if isinstance(mixed, crypto.X509): if isinstance(mixed, crypto.X509):
self._obj = mixed self._obj = mixed
else: else:

View File

@ -1,20 +1,20 @@
cffi==1.2.1 cffi==1.2.1
click==5.1 click==5.1
configparser==3.3.0r2
cryptography==1.0 cryptography==1.0
falcon==0.3.0 falcon==0.3.0
future=0.15.2
humanize==0.5.1 humanize==0.5.1
idna==2.0 idna==2.0
ipaddress==1.0.16
ipsecparse==0.1.0 ipsecparse==0.1.0
Jinja2==2.8 Jinja2==2.8
ldap3==0.9.8.8 Markdown==2.6.5
MarkupSafe==0.23 MarkupSafe==0.23
pyasn1==0.1.8 pyasn1==0.1.8
pycountry==1.14
pycparser==2.14
pycrypto==2.6.1 pycrypto==2.6.1
pykerberos==1.1.8 pykerberos==1.1.8
pyOpenSSL==0.15.1 pyOpenSSL==0.15.1
python-ldap==2.4.10
python-mimeparse==0.1.4 python-mimeparse==0.1.4
requests==2.2.1 requests==2.2.1
setproctitle==1.1.9 setproctitle==1.1.9

View File

@ -36,7 +36,7 @@ setup(
], ],
include_package_data = True, include_package_data = True,
package_data={ package_data={
"certidude": ["certidude/templates/*.html"], "certidude": ["certidude/templates/*"],
}, },
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",