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

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
target/
# npm
node_modules/
# diff
*.diff

View File

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

View File

@ -67,8 +67,11 @@ To install Certidude:
.. code:: bash
apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev libkrb5-dev
pip3 install --allow-external mysql-connector-python mysql-connector-python
apt-get install -y python python-pip python-dev cython \
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
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
adduser --system --no-create-home --group certidude
mkdir /etc/certidude
Setting up CA
@ -144,7 +148,7 @@ Install ``nginx`` and ``uwsgi``:
.. code:: bash
apt-get install nginx uwsgi uwsgi-plugin-python3
apt-get install nginx uwsgi uwsgi-plugin-python
For easy setup following is reccommended:
@ -162,7 +166,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
vaccum = true
uid = certidude
gid = certidude
plugins = python34
plugins = python
chdir = /tmp
module = certidude.wsgi
callable = app
@ -192,7 +196,7 @@ configure the site in /etc/nginx/sites-available/certidude:
server_name localhost;
listen 80 default_server;
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/ {
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
location /pub {
allow 127.0.0.1; # Allow publishing only from CA machine
push_stream_publisher admin;
push_stream_channels_path $arg_id;
allow 127.0.0.1;
nchan_publisher http;
nchan_store_messages off;
nchan_channel_id $arg_id;
}
location ~ "^/lp/(.*)" {
push_stream_channels_path $1;
push_stream_subscriber long-polling;
nchan_subscriber longpoll;
nchan_channel_id $1;
}
location ~ "^/ev/(.*)" {
push_stream_channels_path $1;
push_stream_subscriber eventsource;
nchan_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:
.. code::
push_server = http://push.example.com/
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.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
@ -294,11 +301,36 @@ Set up Samba client configuration in ``/etc/samba/smb.conf``:
realm = EXAMPLE.LAN
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:
.. 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
@ -379,22 +411,29 @@ Clone the repository:
git clone https://github.com/laurivosandi/certidude
cd certidude
Install dependencies as shown above and additionally:
.. code:: bash
pip install -r requirements.txt
To generate templates:
.. code:: bash
apt-get install npm nodejs
npm install nunjucks
nunjucks-precompile --include "\\.html$" --include "\\.svg" certidude/static/ > certidude/static/js/templates.js
sudo ln -s nodejs /usr/bin/node # Fix 'env node' on Ubuntu 14.04
npm install -g nunjucks
nunjucks-precompile --include "\\.html$" --include "\\.svg$" certidude/static/ > certidude/static/js/templates.js
To run from source tree:
.. 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:
.. 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 mimetypes
import logging
import os
import click
from datetime import datetime
from time import sleep
from certidude import authority
from certidude import authority, mailer
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 import config
from certidude import constants, config
logger = logging.getLogger("api")
class CertificateStatusResource(object):
"""
@ -24,7 +30,9 @@ class CertificateStatusResource(object):
class CertificateAuthorityResource(object):
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.append_header("Content-Type", "application/x-x509-ca-cert")
resp.append_header("Content-Disposition", "attachment; filename=ca.crt")
@ -34,16 +42,54 @@ class SessionResource(object):
@authorize_admin
@event_source
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(
username=req.context.get("user"),
event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN,
user = dict(
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,
request_subnets = config.REQUEST_SUBNETS,
admin_subnets=config.ADMIN_SUBNETS,
admin_users=config.ADMIN_USERS,
admin_users = admins,
#admin_users=config.ADMIN_USERS,
authority = dict(
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())
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):
@ -58,7 +104,7 @@ class StaticResource(object):
if os.path.isdir(path):
path = os.path.join(path, "index.html")
print("Serving:", path)
click.echo("Serving: %s" % path)
if os.path.exists(path):
content_type, content_encoding = mimetypes.guess_type(path)
@ -72,7 +118,33 @@ class StaticResource(object):
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():
from certidude import config
from .revoked import RevocationListResource
from .signed import SignedCertificateListResource, SignedCertificateDetailResource
from .request import RequestListResource, RequestDetailResource
@ -82,60 +154,56 @@ def certidude_app():
from .tag import TagResource, TagDetailResource
from .cfg import ConfigResource, ScriptResource
app = falcon.API()
app = falcon.API(middleware=NormalizeMiddleware())
# Certificate authority API calls
app.add_route("/api/ocsp/", CertificateStatusResource())
app.add_route("/api/bundle/", BundleResource())
app.add_route("/api/certificate/", CertificateAuthorityResource())
app.add_route("/api/revoked/", RevocationListResource())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
app.add_route("/api/signed/", SignedCertificateListResource())
app.add_route("/api/request/{cn}/", RequestDetailResource())
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())
# Gateway API calls, should this be moved to separate project?
app.add_route("/api/lease/", LeaseResource())
app.add_route("/api/whois/", WhoisResource())
"""
Set up logging
"""
log_handlers = []
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
from certidude.mysqllog import MySQLLogHandler
from datetime import datetime
import logging
import socket
import json
if config.TAGGING_BACKEND == "sql":
uri = config.cp.get("tagging", "database")
app.add_route("/api/tag/", TagResource(uri))
app.add_route("/api/tag/{identifier}/", TagDetailResource(uri))
app.add_route("/api/config/", ConfigResource(uri))
app.add_route("/api/script/", ScriptResource(uri))
elif config.TAGGING_BACKEND:
raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND)
class PushLogHandler(logging.Handler):
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()))
if config.DATABASE_POOL:
sql_handler = MySQLLogHandler(config.DATABASE_POOL)
push_handler = PushLogHandler()
if config.PUSH_PUBLISH:
from certidude.push import PushLogHandler
log_handlers.append(PushLogHandler())
for facility in "api", "cli":
logger = logging.getLogger(facility)
logger.setLevel(logging.DEBUG)
if config.DATABASE_POOL:
logger.addHandler(sql_handler)
logger.addHandler(push_handler)
for handler in log_handlers:
logger.addHandler(handler)
logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN)
logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN)
import atexit

View File

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

View File

@ -2,38 +2,14 @@
from certidude import config
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
from certidude.relational import RelationalMixin
class LogResource(RelationalMixin):
SQL_CREATE_TABLES = "log_tables.sql"
class LogResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp):
"""
Translate currently online client's IP-address to distinguished name
"""
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
# TODO: Add last id parameter
return self.iterfetch("select * from log order by created desc")

View File

@ -5,45 +5,42 @@ import logging
import ipaddress
import os
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.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types
logger = logging.getLogger("api")
class RequestListResource(object):
@serialize
@login_required
@authorize_admin
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):
"""
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
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")
body = req.stream.read(req.content_length)
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
try:
cert = authority.get_signed(csr.common_name)
except FileNotFoundError:
except EnvironmentError:
pass
else:
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
if req.get_param_as_bool("autosign"):
for subnet in config.AUTOSIGN_SUBNETS:
if subnet.overlaps(remote_addr):
if subnet.overlaps(req.context.get("remote_addr")):
try:
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr).dump()
return
except FileExistsError: # Certificate already exists, try to save the request
except EnvironmentError: # Certificate already exists, try to save the request
pass
break
@ -73,7 +70,8 @@ class RequestListResource(object):
pass
except errors.DuplicateCommonNameError:
# 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(
"CSR with such CN already exists",
"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()
click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url)
logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url)
resp.set_header("Location", url.encode("ascii"))
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
else:
# Request was accepted, but not processed
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):
@ -101,11 +99,8 @@ class RequestDetailResource(object):
Fetch certificate signing request as PEM
"""
csr = authority.get_request(cn)
# if not os.path.exists(path):
# raise falcon.HTTPNotFound()
resp.set_header("Content-Type", "application/pkcs10")
resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name)
logger.debug("Signing request %s was downloaded by %s",
csr.common_name, req.context.get("remote_addr"))
return csr
@login_required
@ -120,14 +115,17 @@ class RequestDetailResource(object):
resp.body = "Certificate successfully signed"
resp.status = falcon.HTTP_201
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
@authorize_admin
def on_delete(self, req, resp, cn):
try:
authority.delete_request(cn)
except FileNotFoundError:
# Logging implemented in the function above
except EnvironmentError as e:
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()

View File

@ -1,9 +1,12 @@
import logging
from certidude.authority import export_crl
logger = logging.getLogger("api")
class RevocationListResource(object):
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.append_header("Content-Disposition", "attachment; filename=ca.crl")
resp.body = export_crl()

View File

@ -9,40 +9,35 @@ logger = logging.getLogger("api")
class SignedCertificateListResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp):
for j in 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())
return {"signed":authority.list_signed()}
class SignedCertificateDetailResource(object):
@serialize
def on_get(self, req, resp, cn):
# Compensate for NTP lag
from time import sleep
sleep(5)
# from time import sleep
# sleep(5)
try:
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"])
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
return authority.get_signed(cn)
except FileNotFoundError:
logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"])
cert = authority.get_signed(cn)
except EnvironmentError:
logger.warning("Failed to serve non-existant certificate %s to %s",
cn, req.context.get("remote_addr"))
resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound()
else:
logger.debug("Served certificate %s to %s",
cn, req.context.get("remote_addr"))
return cert
@login_required
@authorize_admin
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)

View File

@ -1,117 +1,63 @@
import falcon
import logging
from certidude import config
from certidude.relational import RelationalMixin
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
logger = logging.getLogger("api")
SQL_TAG_LIST = """
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
"""
class TagResource(RelationalMixin):
SQL_CREATE_TABLES = "tag_tables.sql"
SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s"
class TagResource(object):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute(SQL_TAG_LIST)
return self.iterfetch("select * from tag")
def g():
for row in cursor:
yield row
cursor.close()
conn.close()
return tuple(g())
@serialize
@login_required
@authorize_admin
def on_post(self, req, resp):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
args = req.get_param("cn"),
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")
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)
conn.commit()
cursor.close()
conn.close()
class TagDetailResource(object):
class TagDetailResource(RelationalMixin):
SQL_CREATE_TABLES = "tag_tables.sql"
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp, identifier):
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute(SQL_TAG_DETAIL, (identifier,))
conn = self.sql_connect()
cursor = conn.cursor()
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:
cursor.close()
conn.close()
return row
return dict(zip(cols, row))
cursor.close()
conn.close()
raise falcon.HTTPNotFound()
@serialize
@login_required
@authorize_admin
def on_put(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
# 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()
args = req.get_param("value"), identifier
self.sql_execute("tag_update.sql", *args)
logger.debug("Tag %s updated, value set to %s",
identifier, req.get_param("value"))
push.publish("tag-updated", identifier)
@ -122,13 +68,6 @@ class TagDetailResource(object):
@authorize_admin
def on_delete(self, req, resp, identifier):
from certidude import push
conn = config.DATABASE_POOL.get_connection()
cursor = conn.cursor()
cursor.execute("delete from device_tag where id = %s", (identifier,))
conn.commit()
cursor.close()
conn.close()
self.sql_execute("tag_delete.sql", identifier)
push.publish("tag-removed", identifier)
logger.debug("Tag %s removed" % identifier)

View File

@ -46,7 +46,7 @@ class WhoisResource(object):
identity = address_to_identity(
conn,
ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"])
req.context.get("remote_addr")
)
conn.close()
@ -55,4 +55,4 @@ class WhoisResource(object):
return dict(address=identity[0], acquired=identity[1], identity=identity[2])
else:
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,108 +1,213 @@
import click
import falcon
import ipaddress
import kerberos
import logging
import os
import re
import socket
from certidude import config
from certidude.firewall import whitelist_subnets
from certidude import config, constants
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]
if config.AUTHENTICATION_BACKEND == "kerberos":
if not os.getenv("KRB5_KTNAME"):
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
ktname = os.getenv("KRB5_KTNAME")
if not ktname:
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
exit(250)
if not os.path.exists(ktname):
click.echo("Kerberos keytab %s does not exist" % ktname, err=True)
exit(248)
try:
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
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)
else:
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
class User(object):
def __init__(self, name):
if "@" in name:
self.mail = name
self.name, self.domain = name.split("@")
else:
NotImplemented
self.mail = None
self.name, self.domain = name, None
self.given_name, self.surname = None, None
def login_required(func):
def pam_authenticate(resource, req, resp, *args, **kwargs):
def __repr__(self):
if self.given_name and self.surname:
return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
else:
return self.mail
def member_of(group_name):
"""
Authenticate against PAM with WWW Basic Auth credentials
Check if requesting user is member of an UNIX group
"""
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 "):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization)
from base64 import b64decode
basic, token = authorization.split(" ", 1)
user, passwd = b64decode(token).split(":", 1)
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
raise falcon.HTTPForbidden("Forbidden", "Invalid password")
req.context["user"] = user
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)
def ldap_account_info(resource, req, resp, *args, **kwargs):
import ldap
import ldap.sasl
if "ldap_conn" not in req.context:
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!")
ft = config.LDAP_USER_FILTER % req.context.get("user").name
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft,
["cn", "givenname", "sn", "mail", "userPrincipalName"])
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)
req.context["user"].dn = dn.decode("utf-8")
req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,)
retval = func(resource, req, resp, *args, **kwargs)
req.context.get("ldap_conn").unbind_s()
return retval
else:
raise ValueError("Failed to look up %s in LDAP" % req.context.get("user"))
if config.ACCOUNTS_BACKEND == "ldap":
return ldap_account_info
elif config.ACCOUNTS_BACKEND == "posix":
return posix_account_info
else:
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):
authorization = req.get_header("Authorization")
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
if not authorization:
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.env["REMOTE_ADDR"])
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?")
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(authorization.split()[1:])
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],))
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:
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]))
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],))
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context)
req.context["user"], req.context["user_realm"] = user.split("@")
req.context["user"] = User(user)
req.context["groups"] = set()
try:
# BUGBUG: https://github.com/02strich/pykerberos/issues/6
#kerberos.authGSSServerClean(context)
pass
kerberos.authGSSServerClean(context)
except kerberos.GSSError as ex:
# TODO: logger.error
raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],))
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.env["REMOTE_ADDR"])
return func(resource, req, resp, *args, **kwargs)
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")
@ -110,35 +215,110 @@ def login_required(func):
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
if config.AUTHENTICATION_BACKEND == "kerberos":
return kerberos_authenticate
elif config.AUTHENTICATION_BACKEND == "pam":
return pam_authenticate
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:
NotImplemented
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 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
if req.context.get("user") not in config.ADMIN_USERS:
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr)
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.get("user"), req.context.get("remote_addr"))
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 requests
from OpenSSL import crypto
from certidude import config, push
from certidude import config, push, mailer
from certidude.wrappers import Certificate, Request
from certidude.signer import raw_sign
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://jamielinux.com/docs/openssl-certificate-authority/
# 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):
# TODO: Implement e-mail and nginx notifications using hooks
def wrapped(csr, *args, **kwargs):
cert = func(csr, *args, **kwargs)
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:
url = config.PUSH_PUBLISH % csr.fingerprint()
click.echo("Publishing certificate at %s ..." % url)
requests.post(url, data=cert.dump(),
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)
return cert
return wrapped
def get_request(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")))
def get_signed(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")))
def get_revoked(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")))
def store_request(buf, overwrite=False):
"""
Store CSR for later processing
@ -92,7 +109,7 @@ def revoke_certificate(common_name):
cert = get_signed(common_name)
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
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):
@ -136,39 +153,50 @@ def delete_request(common_name):
raise ValueError("Invalid common name")
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
request_sha1sum = Request(open(path)).fingerprint()
request = Request(open(path))
os.unlink(path)
# 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
requests.delete(config.PUSH_PUBLISH % request_sha1sum,
requests.delete(config.PUSH_PUBLISH % request.common_name,
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
click.echo("Generating 4096-bit RSA key...")
click.echo("Generating %d-bit RSA key..." % key_size)
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 512)
key.generate_key(crypto.TYPE_RSA, key_size)
# Construct CSR
csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key)
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
cert = sign(Request(buf), overwrite=True)
# Generate P12
ca_certs = crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()),
p12 = crypto.PKCS12()
p12.set_privatekey( key )
p12.set_certificate( cert._obj )
p12.set_ca_certificates( ca_certs )
return p12.export()
p12.set_ca_certificates([certificate._obj])
return p12.export(), cert
@publish_certificate
@ -187,7 +215,7 @@ def sign(req, overwrite=False, delete=True):
elif req.pubkey == old_cert.pubkey:
return old_cert
else:
raise FileExistsError("Will not overwrite existing certificate")
raise EnvironmentError("Will not overwrite existing certificate")
# Sign via signer process
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")
if os.path.exists(path):
if overwrite:
revoke(request.common_name)
revoke_certificate(request.common_name)
else:
raise FileExistsError("File %s already exists!" % path)
raise EnvironmentError("File %s already exists!" % path)
buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
with open(path + ".part", "wb") as fh:

View File

@ -3,7 +3,6 @@
import asyncore
import click
import configparser
import hashlib
import logging
import os
@ -14,11 +13,11 @@ import signal
import socket
import subprocess
import sys
from certidude.signer import SignServer
from certidude.common import expand_paths
from configparser import ConfigParser
from certidude import constants
from certidude.common import expand_paths, ip_address, ip_network
from datetime import datetime
from humanize import naturaltime
from ipaddress import ip_network, ip_address
from jinja2 import Environment, PackageLoader
from time import sleep
from setproctitle import setproctitle
@ -66,7 +65,7 @@ if os.getuid() >= 1000:
def certidude_request_spawn(fork):
from certidude.helpers import certidude_request_certificate
clients = configparser.ConfigParser()
clients = ConfigParser()
clients.readfp(open("/etc/certidude/client.conf"))
services = ConfigParser()
@ -92,7 +91,7 @@ def certidude_request_spawn(fork):
os.kill(pid, signal.SIGTERM)
click.echo("Terminated process %d" % pid)
os.unlink(pid_path)
except (ValueError, ProcessLookupError, FileNotFoundError):
except EnvironmentError:
pass
if fork:
@ -137,7 +136,7 @@ def certidude_request_spawn(fork):
# Set up IPsec via NetworkManager
if services.get(endpoint, "service") == "network-manager/strongswan":
config = configparser.ConfigParser()
config = ConfigParser()
config.add_section("connection")
config.add_section("vpn")
config.add_section("ipv4")
@ -218,6 +217,7 @@ def certidude_signer_spawn(kill, no_interaction):
"""
Spawn privilege isolated signer process
"""
from certidude.signer import SignServer
from certidude import config
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
@ -254,7 +254,7 @@ def certidude_signer_spawn(kill, no_interaction):
pid = int(fh.readline())
os.kill(pid, 0)
click.echo("Found process with PID %d" % pid)
except (ValueError, ProcessLookupError, FileNotFoundError):
except EnvironmentError:
pid = 0
if pid > 0:
@ -265,7 +265,7 @@ def certidude_signer_spawn(kill, no_interaction):
sleep(1)
os.kill(pid, signal.SIGKILL)
sleep(1)
except ProcessLookupError:
except EnvironmentError:
pass
child_pid = os.fork()
@ -280,15 +280,7 @@ def certidude_signer_spawn(kill, no_interaction):
logging.basicConfig(
filename="/var/log/signer.log",
level=logging.INFO)
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)
server = SignServer()
asyncore.loop()
@ -363,8 +355,8 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
common_name,
org_unit,
email_address,
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,ikeIntermediate",
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
wait=True)
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
# 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()
@ -385,6 +377,74 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
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.argument("url")
@click.argument("remote")
@ -419,7 +479,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
return retval
# 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()
@ -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("--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("--subnet", "-s", default="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("--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", 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("--config", "-o",
default="/etc/ipsec.conf",
@ -473,7 +533,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
common_name,
org_unit,
email_address,
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
ip_address=local,
dns=fqdn,
@ -482,7 +542,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
if 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)
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
# 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)
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()
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("vpn")
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))
@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("--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("--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",
default="/etc/nginx/nginx.conf",
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)
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)
if not os.path.exists(kerberos_keytab):
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("/"):
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)
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)
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)
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")
@ -735,6 +813,9 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60)
ca.set_issuer(ca.get_subject())
ca.set_pubkey(key)
# add_extensions shall be called only once and
# there has to be only one subjectAltName!
ca.add_extensions([
crypto.X509Extension(
b"basicConstraints",
@ -746,7 +827,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
b"keyCertSign, cRLSign"),
crypto.X509Extension(
b"extendedKeyUsage",
True,
False,
b"serverAuth,1.3.6.1.5.5.8.2.2"),
crypto.X509Extension(
b"subjectKeyIdentifier",
@ -756,21 +837,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
crypto.X509Extension(
b"crlDistributionPoints",
False,
crl_distribution_points.encode("ascii"))
])
subject_alt_name = "email:%s" % email_address
ca.add_extensions([
crl_distribution_points.encode("ascii")),
crypto.X509Extension(
b"subjectAltName",
False,
subject_alt_name.encode("ascii"))
])
ca.add_extensions([
crypto.X509Extension(
b"subjectAltName",
True,
("DNS:%s" % common_name).encode("ascii"))
"DNS: %s, email: %s" % (common_name.encode("ascii"), email_address.encode("ascii")))
])
if ocsp_responder_url:
@ -819,7 +890,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
# Set permission bits to 640
os.umask(0o137)
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:
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()
@click.command("serve", help="Run built-in HTTP server")
@click.option("-u", "--user", default="certidude", help="Run as user")
@click.option("-p", "--port", default=80, help="Listen port")
@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")
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(
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 socketserver import ThreadingMixIn
from certidude.api import certidude_app, StaticResource
from certidude import config
class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
pass
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.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
"".encode("charmap")
if config.AUTHENTICATION_BACKEND == "pam":
restricted_groups = []
if config.AUTHENTICATION_BACKENDS == {"pam"}:
# PAM needs access to /etc/shadow
import grp
name, passwd, gid, mem = grp.getgrnam("shadow")
click.echo("Adding current user to shadow group due to PAM authentication backend")
os.setgroups([gid])
else:
os.setgroups([])
restricted_groups.append(gid)
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(user)
if uid == 0:
click.echo("Please specify unprivileged user")
exit(254)
restricted_groups.append(gid)
os.setgroups(restricted_groups)
os.setgid(gid)
os.setuid(uid)
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)
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_client)
certidude_setup.add_command(certidude_setup_production)
certidude_setup.add_command(certidude_setup_nginx)
certidude_request.add_command(certidude_request_spawn)
certidude_signer.add_command(certidude_signer_spawn)
entry_point.add_command(certidude_setup)

View File

@ -1,6 +1,13 @@
import os
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():
"""

View File

@ -4,50 +4,52 @@ import codecs
import configparser
import ipaddress
import os
import socket
import string
from random import choice
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.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8"))
AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam
AUTHENTICATION_BACKENDS = set([j for j in
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])
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j])
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)
USER_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "user subnets").split(" ") if j])
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in
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_PID_PATH = "/run/certidude/signer.pid"
AUTHORITY_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private_key_path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate_path")
REQUESTS_DIR = cp.get("authority", "requests_dir")
SIGNED_DIR = cp.get("authority", "signed_dir")
REVOKED_DIR = cp.get("authority", "revoked_dir")
#LOG_DATA = cp.get("logging", "database")
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed dir")
REVOKED_DIR = cp.get("authority", "revoked dir")
OUTBOX = cp.get("authority", "outbox")
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_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 = "ca"
try:
PUSH_EVENT_SOURCE = cp.get("push", "event_source")
PUSH_LONG_POLL = cp.get("push", "long_poll")
PUSH_EVENT_SOURCE = cp.get("push", "event source")
PUSH_LONG_POLL = cp.get("push", "long poll")
PUSH_PUBLISH = cp.get("push", "publish")
except configparser.NoOptionError:
PUSH_SERVER = cp.get("push", "server") or "http://localhost"
@ -55,18 +57,41 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%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:
DATABASE_POOL = None
elif o.scheme == "mysql":
import mysql.connector
DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool(
pool_size = 32,
user=o.username,
password=o.password,
host=o.hostname,
database=o.path[1:])
TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend")
LEASES_BACKEND = cp.get("leases", "backend")
if "whitelist" == AUTHORIZATION_BACKEND:
USERS_WHITELIST = set([j for j in cp.get("authorization", "users whitelist").split(" ") if j])
ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
elif "posix" == AUTHORIZATION_BACKEND:
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:
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 ipaddress
import json
import logging
import re
import types
from datetime import date, time, datetime
from OpenSSL import crypto
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 wrapped(self, req, resp, *args, **kwargs):
@ -15,7 +41,6 @@ def event_source(func):
resp.status = falcon.HTTP_SEE_OTHER
resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid
resp.body = "Redirecting to:" + resp.location
print("Delegating EventSource handling to:", resp.location)
return func(self, req, resp, *args, **kwargs)
return wrapped
@ -24,9 +49,10 @@ class MyEncoder(json.JSONEncoder):
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"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", \
"key_type", "key_length", "sha256sum", "serial_number", "key_usage"
"key_type", "key_length", "sha256sum", "serial_number", "key_usage", \
"signed", "expires"
def default(self, obj):
if isinstance(obj, crypto.X509Name):
@ -60,18 +86,25 @@ def serialize(func):
Falcon response serialization
"""
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("Pragma", "no-cache");
resp.set_header("Expires", "0");
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
resp.set_header("Pragma", "no-cache")
resp.set_header("Expires", "0")
r = func(instance, req, resp, **kwargs)
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-Disposition", "inline")
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:
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 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 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
"""
@ -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)
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)
except crypto.Error:
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:
request = Request(open(request_path))
click.echo("Found signing request: %s" % request_path)
except FileNotFoundError:
except EnvironmentError:
# Construct private 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.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key)
csr.get_subject().CN = common_name
request = Request(csr)
# Set subject attributes
request.common_name = common_name
if given_name:
request.given_name = given_name
if surname:
@ -83,20 +85,20 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
# Collect subject alternative names
subject_alt_name = set()
if email_address:
subject_alt_name.add("email:" + email_address)
subject_alt_name.add("email:%s" % email_address)
if ip_address:
subject_alt_name.add("IP:" + ip_address)
subject_alt_name.add("IP:%s" % ip_address)
if dns:
subject_alt_name.add("DNS:" + dns)
subject_alt_name.add("DNS:%s" % dns)
# Set extensions
extensions = []
if key_usage:
extensions.append(("keyUsage", key_usage, True))
if extended_key_usage:
extensions.append(("extendedKeyUsage", extended_key_usage, True))
extensions.append(("extendedKeyUsage", extended_key_usage, False))
if subject_alt_name:
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True))
extensions.append(("subjectAltName", ", ".join(subject_alt_name), False))
request.set_extensions(extensions)
# 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)
submission = requests.post(request_url,
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:
pass
@ -131,12 +133,18 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
try:
cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % buf)
raise ValueError("Failed to parse PEM: %s" % submission.text)
os.umask(0o022)
with open(certificate_path + ".part", "w") as fh:
# Dump certificate
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)
os.rename(certificate_path + ".part", certificate_path)

View File

@ -1,15 +1,25 @@
import os
import smtplib
from time import sleep
from markdown import markdown
from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from urllib.parse import urlparse
class Mailer(object):
def __init__(self, url):
scheme, netloc, path, params, query, fragment = urlparse(url)
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
def send(recipients, template, attachments=(), **context):
from certidude import authority, config
if not config.OUTBOX:
# Mailbox disabled, don't send e-mail
return
if not recipients:
raise ValueError("No e-mail recipients specified!")
scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
scheme = scheme.lower()
if path:
@ -22,15 +32,15 @@ class Mailer(object):
raise ValueError("Fragment for URL not supported")
self.username = None
self.password = ""
username = None
password = ""
if scheme == "smtp":
self.secure = False
self.port = 25
secure = False
port = 25
elif scheme == "smtps":
self.secure = True
self.port = 465
secure = True
port = 465
else:
raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme)
@ -38,51 +48,24 @@ class Mailer(object):
credentials, netloc = netloc.split("@")
if ":" in credentials:
self.username, self.password = credentials.split(":")
username, password = credentials.split(":")
else:
self.username = credentials
username = credentials
if ":" in netloc:
self.server, port_str = netloc.split(":")
self.port = int(port_str)
server, port_str = netloc.split(":")
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)
server = netloc
def send(self, sender, recipients, subject, template, **context):
recipients = [j for j in recipients if j]
if not recipients:
print("No recipients to send e-mail to!")
return
print("Sending e-mail to:", recipients, "body follows:")
subject, text = env.get_template(template).render(context).split("\n\n", 1)
html = markdown(text)
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = sender
msg["To"] = ", ".join(recipients)
text = self.env.get_template(template + ".txt").render(context)
html = self.env.get_template(template + ".html").render(context)
print(text)
msg["From"] = authority.certificate.email_address
msg["To"] = recipients
part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
@ -90,15 +73,18 @@ class Mailer(object):
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)
for attachment in attachments:
part = MIMEBase(*attachment.content_type.split("/"))
part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename)
part.set_payload(attachment.dump())
msg.attach(part)
# 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)
conn.sendmail(authority.certificate.email_address, recipients, msg.as_string())

View File

@ -1,34 +1,17 @@
import logging
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(
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):
def __init__(self, uri):
logging.Handler.__init__(self)
self.pool = pool
conn = self.pool.get_connection()
cur = conn.cursor()
cur.execute(self.SQL_CREATE_TABLE)
conn.commit()
cur.close()
conn.close()
RelationalMixin.__init__(self, uri)
def emit(self, record):
conn = self.pool.get_connection()
cur = conn.cursor()
cur.execute(self.SQL_INSERT_ENTRY, (
self.sql_execute("log_insert_entry.sql",
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)),
record.name,
record.levelno,
@ -39,7 +22,4 @@ class MySQLLogHandler(logging.Handler):
logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "",
record.process,
record.thread,
record.threadName))
conn.commit()
cur.close()
conn.close()
record.threadName)

View File

@ -1,7 +1,9 @@
import click
import json
import logging
import requests
from datetime import datetime
from certidude import config
@ -9,13 +11,29 @@ def publish(event_type, event_data):
"""
Publish event on push server
"""
if not isinstance(event_data, str):
if not isinstance(event_data, basestring):
from certidude.decorators import MyEncoder
event_data = json.dumps(event_data, cls=MyEncoder)
url = config.PUSH_PUBLISH % config.PUSH_TOKEN
click.echo("Publishing %s event %s on %s" % (event_type, event_data, url))
try:
notification = requests.post(
config.PUSH_PUBLISH % config.PUSH_TOKEN,
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 asyncore
import asynchat
from certidude import constants, config
from datetime import datetime
from OpenSSL import crypto
@ -26,8 +27,6 @@ certificate authoirty (basicConstraints=CA:TRUE) or
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):
"""
Sign certificate signing request directly with private key assuming it's readable by the process
@ -43,12 +42,6 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa
# Set issuer
cert.set_issuer(ca_cert.get_subject())
# TODO: Assert openssl.cnf policy for subject attributes
# if request.get_subject().O != ca_cert.get_subject().O:
# raise ValueError("Orgnization name mismatch!")
# if request.get_subject().C != ca_cert.get_subject().C:
# raise ValueError("Country mismatch!")
# Copy attributes from CA
if ca_cert.get_subject().C:
cert.get_subject().C = ca_cert.get_subject().C
@ -61,8 +54,13 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa
# Copy attributes from request
cert.get_subject().CN = request.get_subject().CN
req_subject = request.get_subject()
if hasattr(req_subject, "OU") and req_subject.OU:
if request.get_subject().SN:
cert.get_subject().SN = request.get_subject().SN
if request.get_subject().GN:
cert.get_subject().GN = request.get_subject().GN
if request.get_subject().OU:
cert.get_subject().OU = req_subject.OU
# Copy e-mail, key usage, extended key from request
@ -104,7 +102,7 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa
cert.set_serial_number(random.randint(
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff))
cert.sign(private_key, 'sha1')
cert.sign(private_key, 'sha256')
return cert
@ -128,7 +126,7 @@ class SignHandler(asynchat.async_chat):
serial_number, timestamp = line.split(":")
# TODO: Assert serial against regex
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_serial(serial_number.encode("ascii"))
crl.add_revoked(revocation)
@ -137,7 +135,7 @@ class SignHandler(asynchat.async_chat):
self.server.certificate,
self.server.private_key,
crypto.FILETYPE_PEM,
self.server.revocation_list_lifetime))
config.REVOCATION_LIST_LIFETIME))
elif cmd == "ocsp-request":
NotImplemented # TODO: Implement OCSP
@ -147,7 +145,7 @@ class SignHandler(asynchat.async_chat):
for e in request.get_extensions():
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)
# TODO: Potential exploits during PEM parsing?
@ -155,10 +153,10 @@ class SignHandler(asynchat.async_chat):
self.server.private_key,
self.server.certificate,
request,
basic_constraints=self.server.basic_constraints,
key_usage=self.server.key_usage,
extended_key_usage=self.server.extended_key_usage,
lifetime=self.server.lifetime)
basic_constraints=config.CERTIFICATE_BASIC_CONSTRAINTS,
key_usage=config.CERTIFICATE_KEY_USAGE_FLAGS,
extended_key_usage=config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
lifetime=config.CERTIFICATE_LIFETIME)
self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
else:
raise NotImplementedError("Unknown command: %s" % cmd)
@ -175,26 +173,23 @@ class SignHandler(asynchat.async_chat):
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)
# Bind to sockets
if os.path.exists(socket_path):
os.unlink(socket_path)
if os.path.exists(config.SIGNER_SOCKET_PATH):
os.unlink(config.SIGNER_SOCKET_PATH)
os.umask(0o007)
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.bind(socket_path)
self.bind(config.SIGNER_SOCKET_PATH)
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
# (<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%;
}
ul {
list-style: none;
margin: 1em 0;
padding: 0;
}
#pending_requests .notify {
display: none;
}
@ -142,7 +136,17 @@ pre {
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;
padding: 4px 0;
clear: both;
@ -164,7 +168,8 @@ pre {
.icon{
background-size: 24px;
padding-left: 36px;
background-position: 6px 2px;
padding-left: 32px;
background-repeat: no-repeat;
display: block;
vertical-align: text-bottom;
@ -172,7 +177,7 @@ pre {
}
#log_entries li span.icon {
background-size: 32px;
background-size: 24px;
padding-left: 42px;
padding-top: 2px;
padding-bottom: 2px;
@ -180,7 +185,8 @@ pre {
.tags .tag {
display: inline;
background-size: 32px;
background-size: 24px;
background-position: 0 4px;
padding-top: 4px;
padding-bottom: 4px;
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.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); }
.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); }
.icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); }
.icon.error { background-image: url("../img/iconmonstr-error-4.svg"); }
.icon.warning { background-image: url("../img/iconmonstr-warning-8.svg"); }
.icon.info { background-image: url("../img/iconmonstr-info-8.svg"); }
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); }
.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); }
.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); }
.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); }
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-8.svg"); }
.icon.download { background-image: url("../img/iconmonstr-download-12.svg"); }
.icon.sign { background-image: url("../img/iconmonstr-pen-14.svg"); }
.icon.search { background-image: url("../img/iconmonstr-magnifier-4.svg"); }
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); }
.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); }
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); }
.icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); }
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-7.svg"); }
.icon.location { background-image: url("../img/iconmonstr-compass-7.svg"); }
.icon.room { background-image: url("../img/iconmonstr-home-7.svg"); }
.icon.serial { background-image: url("../img/iconmonstr-barcode-4.svg"); }
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); }
.icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); }
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6.svg"); }
.icon.password { background-image: url("../img/iconmonstr-lock-3.svg"); }
/* Make sure this is the last one */
.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>
<nav id="menu">
<ul class="container">
<li data-section="requests">Requests</li>
<li data-section="signed">Signed</li>
<li data-section="revoked">Revoked</li>
<li data-section="config">Configuration</li>
<li data-section="log">Log</li>
<li data-section="about">Profile</li>
<li id="section-requests" data-section="requests" style="display:none;">Requests</li>
<li id="section-signed" data-section="signed" style="display:none;">Signed</li>
<li id="section-revoked" data-section="revoked" style="display:none;">Revoked</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>
</nav>
<div id="container" class="container">

View File

@ -67,7 +67,6 @@ function onRequestSubmitted(e) {
url: "/api/request/" + e.data + "/",
dataType: "json",
success: function(request, status, xhr) {
console.info(request);
$("#pending_requests").prepend(
nunjucks.render('views/request.html', { request: request }));
}
@ -75,12 +74,12 @@ function onRequestSubmitted(e) {
}
function onRequestDeleted(e) {
console.log("Removing deleted request #" + e.data);
$("#request_" + e.data).remove();
console.log("Removing deleted request", e.data);
$("#request-" + e.data.replace("@", "--").replace(".", "-")).remove();
}
function onClientUp(e) {
console.log("Adding security association:" + e.data);
console.log("Adding security association:", e.data);
var lease = JSON.parse(e.data);
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('views/status.html', {
@ -93,7 +92,7 @@ function onClientUp(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 $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
$status.html(nunjucks.render('views/status.html', {
@ -107,7 +106,9 @@ function onClientDown(e) {
function onRequestSigned(e) {
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({
method: "GET",
@ -121,13 +122,14 @@ function onRequestSigned(e) {
});
}
function onCertificateRevoked(e) {
console.log("Removing revoked certificate #" + e.data);
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
console.log("Removing revoked certificate", e.data);
$("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
}
function onTagAdded(e) {
console.log("Tag added #" + e.data);
console.log("Tag added", e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
@ -143,12 +145,12 @@ function onTagAdded(e) {
}
function onTagRemoved(e) {
console.log("Tag removed #" + e.data);
console.log("Tag removed", e.data);
$("#tag_" + e.data).remove();
}
function onTagUpdated(e) {
console.log("Tag updated #" + e.data);
console.log("Tag updated", e.data);
$.ajax({
method: "GET",
url: "/api/tag/" + e.data + "/",
@ -175,9 +177,26 @@ $(document).ready(function() {
$("#container").html(nunjucks.render('views/error.html', { message: msg }));
},
success: function(session, status, xhr) {
console.info("Opening EventSource from:", session.event_channel);
$("#login").hide();
var source = new EventSource(session.event_channel);
/**
* Render authority views
**/
$("#container").html(nunjucks.render('views/authority.html', { session: session, window: window }));
if (session.authority) {
$("#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);
@ -194,13 +213,13 @@ $(document).ready(function() {
source.addEventListener("tag-removed", onTagRemoved);
source.addEventListener("tag-updated", onTagUpdated);
/**
* Render authority views
**/
$("#container").html(nunjucks.render('views/authority.html', { session: session, window: window }));
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) {
$("section").hide();
@ -231,14 +250,16 @@ $(document).ready(function() {
});
console.log("Features enabled:", session.features);
if (session.features.tagging) {
console.info("Tagging enabled");
$("#section-config").show();
$.ajax({
method: "GET",
url: "/api/config/",
dataType: "json",
success: function(configuration, status, xhr) {
console.info("Appending " + configuration.length + " configuration items");
console.info("Appending", configuration.length, "configuration items");
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
/**
* Fetch tags for certificates
@ -249,6 +270,7 @@ $(document).ready(function() {
dataType: "json",
success:function(tags, status, xhr) {
console.info("Got", tags.length, "tags");
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
for (var j = 0; j < tags.length; j++) {
// TODO: Deduplicate
$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>");
@ -262,10 +284,12 @@ $(document).ready(function() {
});
}
});
}
/**
* Fetch leases associated with certificates
*/
if (session.features.leases) {
$.ajax({
method: "GET",
url: "/api/lease/",
@ -290,10 +314,13 @@ $(document).ready(function() {
}
});
return;
}
/**
* Fetch log entries
*/
if (session.features.logging) {
$("#section-log").show();
$.ajax({
method: "GET",
url: "/api/log/",
@ -314,5 +341,6 @@ $(document).ready(function() {
}
});
}
}
});
});

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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
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=\"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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
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=\"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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
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=\"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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
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=\"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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
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=\"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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} 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) {
var lineno = null;
var colno = null;
var output = "";
try {
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";
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";
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 {
@ -461,7 +437,7 @@ var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
@ -485,12 +461,38 @@ var colno = null;
var output = "";
try {
var parentTemplate = null;
output += "\n<section id=\"about\">\n<p>Hi ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"username"), env.opts.autoescape);
output += ",</p>\n\n<p>Request submission is allowed from: ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets")) {
output += "\n<section id=\"about\">\n<h2>";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"gn"), env.opts.autoescape);
output += " ";
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();
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;
for(var t_1=0; t_1 < t_3.length; 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.last", t_1 === t_2 - 1);
frame.set("loop.length", t_2);
output += "\n <li>";
output += runtime.suppressValue(t_4, env.opts.autoescape);
output += " ";
output += "</li>\n ";
;
}
}
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 {
output += "anywhere";
;
}
output += "</p>\n<p>Autosign is allowed from: ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets")) {
output += "\n </p>\n <ul>\n ";
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;
for(var t_5=0; t_5 < t_7.length; 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.index0", 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.last", t_5 === t_6 - 1);
frame.set("loop.length", t_6);
output += "\n <li>";
output += runtime.suppressValue(t_8, env.opts.autoescape);
output += " ";
output += "</li>\n ";
;
}
}
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 {
output += "nowhere";
;
}
output += "</p>\n<p>Authority administration is allowed from: ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets")) {
output += "\n </p>\n <ul>\n ";
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;
for(var t_9=0; t_9 < t_11.length; 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.index0", 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.last", t_9 === t_10 - 1);
frame.set("loop.length", t_10);
output += "\n <li>";
output += runtime.suppressValue(t_12, env.opts.autoescape);
output += " ";
output += "</li>\n ";
;
}
}
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 {
output += "anywhere";
;
}
output += "\n<p>Authority administration allowed for: ";
output += "\n <ul>\n ";
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;
for(var t_13=0; t_13 < t_15.length; 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.index0", 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.last", t_13 === t_14 - 1);
frame.set("loop.length", t_14);
output += "\n <li>";
output += runtime.suppressValue(t_16, env.opts.autoescape);
output += " ";
output += "</li>\n ";
;
}
}
frame = frame.pop();
output += "</p>\n</section>\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);
output += "\n </ul>\n";
;
}
if(frame.topLevel) {
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 ";
output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n";
frame = frame.push();
var t_20 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"requests");
runtime.asyncEach(t_20, 1, function(request, t_18, t_19,next) {
frame.set("request", request);
frame.set("loop.index", t_18 + 1);
frame.set("loop.index0", t_18);
frame.set("loop.revindex", t_19 - t_18);
frame.set("loop.revindex0", t_19 - t_18 - 1);
frame.set("loop.first", t_18 === 0);
frame.set("loop.last", t_18 === t_19 - 1);
frame.set("loop.length", t_19);
output += "\n ";
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_23,t_21) {
if(t_23) { cb(t_23); return; }
t_21.render(context.getVariables(), frame, function(t_24,t_22) {
if(t_24) { cb(t_24); return; }
output += t_22
output += "\n\t ";
next(t_18);
})});
}, function(t_26,t_25) {
if(t_26) { cb(t_26); return; }
var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
if(t_19) {var t_17;
if(runtime.isArray(t_19)) {
var t_18 = t_19.length;
for(t_17=0; t_17 < t_19.length; t_17++) {
var t_20 = t_19[t_17][0]
frame.set("handle", t_19[t_17][0]);
var t_21 = t_19[t_17][1]
frame.set("full_name", t_19[t_17][1]);
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_21, env.opts.autoescape);
output += "</li>\n";
;
}
} 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();
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 += "</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();
var t_29 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"signed")));
runtime.asyncEach(t_29, 1, function(certificate, t_27, t_28,next) {
frame.set("certificate", certificate);
frame.set("loop.index", t_27 + 1);
frame.set("loop.index0", t_27);
frame.set("loop.revindex", t_28 - t_27);
frame.set("loop.revindex0", t_28 - t_27 - 1);
frame.set("loop.first", t_27 === 0);
frame.set("loop.last", t_27 === t_28 - 1);
frame.set("loop.length", t_28);
var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_27) {var t_26 = t_27.length;
for(var t_25=0; t_25 < t_27.length; t_25++) {
var t_28 = t_27[t_25];
frame.set("request", t_28);
frame.set("loop.index", t_25 + 1);
frame.set("loop.index0", t_25);
frame.set("loop.revindex", t_26 - t_25);
frame.set("loop.revindex0", t_26 - t_25 - 1);
frame.set("loop.first", t_25 === 0);
frame.set("loop.last", t_25 === t_26 - 1);
frame.set("loop.length", t_26);
output += "\n ";
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_32,t_30) {
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; }
t_30.render(context.getVariables(), frame, function(t_33,t_31) {
if(t_33) { cb(t_33); return; }
output += t_31
output += t_30
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();
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);
@ -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 += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n ";
frame = frame.push();
var t_38 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"revoked");
if(t_38) {var t_37 = t_38.length;
for(var t_36=0; t_36 < t_38.length; t_36++) {
var t_39 = t_38[t_36];
frame.set("j", t_39);
frame.set("loop.index", t_36 + 1);
frame.set("loop.index0", t_36);
frame.set("loop.revindex", t_37 - t_36);
frame.set("loop.revindex0", t_37 - t_36 - 1);
frame.set("loop.first", t_36 === 0);
frame.set("loop.last", t_36 === t_37 - 1);
frame.set("loop.length", t_37);
var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
if(t_43) {var t_42 = t_43.length;
for(var t_41=0; t_41 < t_43.length; t_41++) {
var t_44 = t_43[t_41];
frame.set("j", t_44);
frame.set("loop.index", t_41 + 1);
frame.set("loop.index0", t_41);
frame.set("loop.revindex", t_42 - t_41);
frame.set("loop.revindex0", t_42 - t_41 - 1);
frame.set("loop.first", t_41 === 0);
frame.set("loop.last", t_41 === t_42 - 1);
frame.set("loop.length", t_42);
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 += runtime.suppressValue(runtime.memberLookup((t_39),"changed"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape);
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 += 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 ";
;
}
}
if (!t_37) {
if (!t_42) {
output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
}
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
})});
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
@ -894,8 +970,8 @@ var colno = null;
var output = "";
try {
var parentTemplate = null;
output += "<li id=\"request_";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
output += "<li id=\"request-";
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 += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
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 += 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";
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; }
t_1.render(context.getVariables(), frame, function(t_4,t_2) {
if(t_4) { cb(t_4); return; }
@ -920,9 +996,9 @@ output += t_2
output += "\n";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape);
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\">";
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; }
t_5.render(context.getVariables(), frame, function(t_8,t_6) {
if(t_8) { cb(t_8); return; }
@ -930,12 +1006,10 @@ output += t_6
output += " ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape);
output += "</div>\n";
cb()})});
})});
}
else {
cb()}
})(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) {
output += "\n\n<div class=\"monospace\">\n";
env.getTemplate("img/iconmonstr-key-3.svg", false, "views/request.html", null, function(t_11,t_9) {
if(t_11) { cb(t_11); return; }
t_9.render(context.getVariables(), frame, function(t_12,t_10) {
if(t_12) { cb(t_12); return; }
@ -957,9 +1031,9 @@ if(frame.topLevel) {
context.addExport("key_usage", t_13);
}
output += "\n";
(function(cb) {if(runtime.contextOrFrameLookup(context, frame, "key_usage")) {
if(runtime.contextOrFrameLookup(context, frame, "key_usage")) {
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; }
t_14.render(context.getVariables(), frame, function(t_17,t_15) {
if(t_17) { cb(t_17); return; }
@ -967,17 +1041,15 @@ output += t_15
output += "\n";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape);
output += "\n</div>\n";
cb()})});
})});
}
else {
cb()}
})(function() {output += "\n\n</li>\n\n";
output += "\n\n</li>\n\n";
if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
})})})})})});
})})})});
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
@ -995,8 +1067,8 @@ var colno = null;
var output = "";
try {
var parentTemplate = null;
output += "<li id=\"certificate_";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"sha256sum"), env.opts.autoescape);
output += "<li id=\"certificate-";
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 += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape);
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 += 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 ";
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; }
t_1.render(context.getVariables(), frame, function(t_4,t_2) {
if(t_4) { cb(t_4); return; }
output += t_2
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 ";
(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\">";
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; }
t_5.render(context.getVariables(), frame, function(t_8,t_6) {
if(t_8) { cb(t_8); return; }
@ -1024,32 +1096,53 @@ output += t_6
output += " ";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape);
output += "</div>\n ";
cb()})});
})});
}
else {
cb()}
})(function() {output += "\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_11,t_9) {
output += "\n \n ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
output += "\n <div class=\"person\">";
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {
if(t_11) { cb(t_11); return; }
t_9.render(context.getVariables(), frame, function(t_12,t_10) {
if(t_12) { cb(t_12); return; }
output += t_10
output += "\n </select>\n </div>\n\n <div class=\"status\">\n ";
env.getTemplate("views/status.html", false, "views/signed.html", null, function(t_15,t_13) {
output += " ";
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; }
t_13.render(context.getVariables(), frame, function(t_16,t_14) {
if(t_16) { cb(t_16); return; }
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
})})})})})})});
})})})})})});
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
@ -1135,6 +1228,44 @@ return {
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() {
@ -1144,7 +1275,7 @@ var colno = null;
var output = "";
try {
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) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {

View File

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

View File

@ -1,36 +1,127 @@
<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>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
<p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
<p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p>
</section>
{% set s = session.certificate.identity %}
<p>Mails will be sent to: {{ session.user.mail }}</p>
<p>You can click <a href="/api/bundle/">here</a> to generate bundle
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>
<section id="requests">
<h1>Pending requests</h1>
<h2>Authority settings</h2>
<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>
<ul id="pending_requests">
{% for request in session.requests %}
{% include "views/request.html" %}
<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 %}
<li class="notify">
<p>No certificate signing requests to sign! You can submit a certificate signing request by:</p>
<pre>certidude setup client {{session.common_name}}</pre>
</li>
</ul>
</section>
{% else %}
<p>Here you can renew your certificates</p>
{% endif %}
{% set s = session.certificate.identity %}
{% if session.authority %}
<section id="requests">
<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">
{% for request in session.authority.requests %}
{% include "views/request.html" %}
{% endfor %}
<li class="notify">
<p>No certificate signing requests to sign!</p>
</li>
</ul>
</section>
<section id="signed">
<h1>Signed certificates</h1>
<input id="search" type="search" class="icon search">
<ul id="signed_certificates">
{% for certificate in session.signed | sort | reverse %}
{% for certificate in session.authority.signed | sort | reverse %}
{% include "views/signed.html" %}
{% endfor %}
</ul>
@ -62,7 +153,7 @@
</pre>
-->
<ul>
{% for j in session.revoked %}
{% for j in session.authority.revoked %}
<li id="certificate_{{ j.sha256sum }}">
{{j.changed}}
{{j.serial_number}} <span class="monospace">{{j.identity}}</span>
@ -75,3 +166,5 @@
<section id="config">
</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>
{% if request.signable %}
@ -10,16 +10,16 @@
<div class="monospace">
{% include 'img/iconmonstr-certificate-15-icon.svg' %}
{% include 'img/iconmonstr-certificate-15.svg' %}
{{request.identity}}
</div>
{% 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 %}
<div class="monospace">
{% include 'img/iconmonstr-key-2-icon.svg' %}
{% include 'img/iconmonstr-key-3.svg' %}
<span title="SHA-1 of public key">
{{ request.sha256sum }}
</span>
@ -30,7 +30,7 @@
{% set key_usage = request.key_usage %}
{% if key_usage %}
<div>
{% include 'img/iconmonstr-flag-3-icon.svg' %}
{% include 'img/iconmonstr-flag-3.svg' %}
{{request.key_usage}}
</div>
{% 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>
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
<div class="monospace">
{% include 'img/iconmonstr-certificate-15-icon.svg' %}
{{certificate.identity}}
{% include 'img/iconmonstr-certificate-15.svg' %}
{{certificate.common_name}}
</div>
{% 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 %}
{% 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">
{% include 'img/iconmonstr-key-2-icon.svg' %}
{% include 'img/iconmonstr-key-3.svg' %}
<span title="SHA-256 of public key">
{{ certificate.sha256sum }}
</span>
@ -23,7 +33,7 @@
</div>
<div>
{% include 'img/iconmonstr-flag-3-icon.svg' %}
{% include 'img/iconmonstr-flag-3.svg' %}
{{certificate.key_usage}}
</div>
@ -35,8 +45,5 @@
{% include 'views/tagtypes.html' %}
</select>
</div>
<div class="status">
{% include 'views/status.html' %}
</div>
<div class="status"></div>
</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.name">Protected 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.channelb">2.4GHz channel number</option>
<option value="wireless.channel">Channel number</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]
admin_users = administrator
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
backend = posix
#backend = ldap
whitelist admin users = root administrator
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]
certificate_lifetime = 1825
revocation_list_lifetime = 1
certificate lifetime = 1825
revocation list lifetime = 1
[push]
server =
[authority]
private_key_path = {{ ca_key }}
certificate_path = {{ ca_crt }}
requests_dir = {{ directory }}/requests/
signed_dir = {{ directory }}/signed/
revoked_dir = {{ directory }}/revoked/
private key path = {{ ca_key }}
certificate path = {{ ca_crt }}
requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/
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 %}
location ~ /publish/(.*) {
location /pub {
allow 127.0.0.1;
push_stream_publisher admin;
push_stream_channels_path $1;
nchan_publisher http;
nchan_store_messages off;
nchan_channel_id $arg_id;
}
location ~ /subscribe/(.*) {
push_stream_channels_path $1;
push_stream_subscriber long-polling;
location ~ "^/lp/(.*)" {
nchan_subscriber longpoll;
nchan_channel_id $1;
}
location ~ "^/ev/(.*)" {
nchan_subscriber eventsource;
nchan_channel_id $1;
}
{% endif %}

View File

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

View File

@ -3,10 +3,9 @@ import hashlib
import re
import click
import io
from Crypto.Util import asn1
from certidude import constants
from OpenSSL import crypto
from datetime import datetime
from certidude.signer import raw_sign, EXTENSION_WHITELIST
def subject2dn(subject):
bits = []
@ -16,6 +15,10 @@ def subject2dn(subject):
return ", ".join(bits)
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):
return self.buf
@ -41,7 +44,7 @@ class CertificateBase:
@common_name.setter
def common_name(self, value):
return setattr(self._obj.get_subject(), "CN", value)
self.subject.CN = value
@property
def country_code(self):
@ -130,7 +133,6 @@ class CertificateBase:
def set_extensions(self, extensions):
# X509Req().add_extensions() first invocation takes only effect?!
assert self._obj.get_extensions() == [], "Extensions already set!"
self._obj.add_extensions([
crypto.X509Extension(
key.encode("ascii"),
@ -164,6 +166,7 @@ class CertificateBase:
@property
def pubkey(self):
from Crypto.Util import asn1
pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
pubkey_der=asn1.DerSequence()
pubkey_der.decode(pubkey_asn1)
@ -194,6 +197,11 @@ class CertificateBase:
class Request(CertificateBase):
@property
def suggested_filename(self):
return self.common_name + ".csr"
def __init__(self, mixed=None):
self.buf = None
self.path = NotImplemented
@ -204,27 +212,23 @@ class Request(CertificateBase):
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
self.created = datetime.fromtimestamp(mtime)
mixed = mixed.read()
if isinstance(mixed, bytes):
mixed = mixed.decode("ascii")
if isinstance(mixed, str):
try:
self.buf = mixed
mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed)
except crypto.Error:
print("Failed to parse:", mixed)
raise
raise ValueError("Failed to parse: %s" % mixed)
if isinstance(mixed, crypto.X509Req):
self._obj = mixed
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()))
@property
def signable(self):
for key, value, data in self.extensions:
if key not in EXTENSION_WHITELIST:
if key not in constants.EXTENSION_WHITELIST:
return False
return True
@ -243,6 +247,11 @@ class Request(CertificateBase):
class Certificate(CertificateBase):
@property
def suggested_filename(self):
return self.common_name + ".crt"
def __init__(self, mixed):
self.buf = NotImplemented
self.path = NotImplemented
@ -253,15 +262,12 @@ class Certificate(CertificateBase):
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
self.changed = datetime.fromtimestamp(mtime)
mixed = mixed.read()
if isinstance(mixed, str):
try:
self.buf = mixed
mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed)
except crypto.Error:
print("Failed to parse:", mixed)
raise
raise ValueError("Failed to parse: %s" % mixed)
if isinstance(mixed, crypto.X509):
self._obj = mixed
else:

View File

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

View File

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