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
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -54,3 +54,9 @@ docs/_build/ | ||||
|  | ||||
| # PyBuilder | ||||
| target/ | ||||
|  | ||||
| # npm | ||||
| node_modules/ | ||||
|  | ||||
| # diff | ||||
| *.diff | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										75
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						| @@ -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 / | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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) | ||||
| else: | ||||
|     NotImplemented | ||||
|  | ||||
| def login_required(func): | ||||
|     def pam_authenticate(resource, req, resp, *args, **kwargs): | ||||
|  | ||||
| class User(object): | ||||
|     def __init__(self, name): | ||||
|         if "@" in name: | ||||
|             self.mail = name | ||||
|             self.name, self.domain = name.split("@") | ||||
|         else: | ||||
|             self.mail = None | ||||
|             self.name, self.domain = name, None | ||||
|         self.given_name, self.surname = None, None | ||||
|  | ||||
|     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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										199
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						| @@ -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) | ||||
|   | ||||
| @@ -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(): | ||||
|     """ | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						| @@ -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"]) | ||||
| @@ -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
									
								
							
							
						
						| @@ -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 | ||||
|  | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						| @@ -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 | ||||
| @@ -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') | ||||
|   | ||||
							
								
								
									
										27
									
								
								certidude/sql/mysql/log_insert_entry.sql
									
									
									
									
									
										Normal 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 | ||||
| ); | ||||
							
								
								
									
										14
									
								
								certidude/sql/mysql/log_tables.sql
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
							
								
								
									
										9
									
								
								certidude/sql/mysql/tag_insert.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| insert into tag ( | ||||
|     `cn`, | ||||
|     `key`, | ||||
|     `value` | ||||
| ) values ( | ||||
|     %s, | ||||
|     %s, | ||||
|     %s | ||||
| ) | ||||
							
								
								
									
										0
									
								
								certidude/sql/mysql/tag_tables.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										27
									
								
								certidude/sql/sqlite/log_insert_entry.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| insert into log ( | ||||
|     created, | ||||
|     facility, | ||||
|     level, | ||||
|     severity, | ||||
|     message, | ||||
|     module, | ||||
|     func, | ||||
|     lineno, | ||||
|     exception, | ||||
|     process, | ||||
|     thread, | ||||
|     thread_name | ||||
| ) values ( | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ?, | ||||
|     ? | ||||
| ); | ||||
							
								
								
									
										14
									
								
								certidude/sql/sqlite/log_tables.sql
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
							
								
								
									
										3
									
								
								certidude/sql/sqlite/tag_delete.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| delete from tag | ||||
| where id = ? | ||||
| limit 1 | ||||
							
								
								
									
										9
									
								
								certidude/sql/sqlite/tag_insert.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| insert into tag ( | ||||
|     `cn`, | ||||
|     `key`, | ||||
|     `value` | ||||
| ) values ( | ||||
|     ?, | ||||
|     ?, | ||||
|     ? | ||||
| ); | ||||
							
								
								
									
										16
									
								
								certidude/sql/sqlite/tag_list.sql
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										36
									
								
								certidude/sql/sqlite/tag_tables.sql
									
									
									
									
									
										Normal 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`) | ||||
| ); | ||||
|  | ||||
| */ | ||||
							
								
								
									
										4
									
								
								certidude/sql/sqlite/tag_update.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| update `tag` | ||||
| set `value` = ? | ||||
| where `id` = ? | ||||
| limit 1 | ||||
| @@ -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");} | ||||
|   | ||||
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-barcode-4.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-calendar-6.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-certificate-15.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-compass-7.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-download-12.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-email-2.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-error-4.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-flag-3.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-home-7.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-info-8.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-key-3.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-magnifier-4.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-mobile-phone-7.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-pen-14.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-tag-3.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-user-5.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-warning-8.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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 | 
| @@ -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 | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-x-mark-8.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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"> | ||||
|   | ||||
| @@ -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() { | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										4
									
								
								certidude/static/js/nunjucks-slim.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										4
									
								
								certidude/static/js/nunjucks.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -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 { | ||||
|   | ||||
							
								
								
									
										2
									
								
								certidude/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| User-agent: * | ||||
| Disallow: / | ||||
| @@ -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> | ||||
| <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> | ||||
|  | ||||
|  | ||||
| <h2>Authority settings</h2> | ||||
|  | ||||
| <p>These can be reconfigured via /etc/certidude/server.conf on the server.</p> | ||||
|  | ||||
| <p>Outgoing mail server: | ||||
| {% if session.authority.outbox %} | ||||
|     {{ session.authority.outbox }} | ||||
| {% else %} | ||||
|     E-mail disabled | ||||
| {% endif %}</p> | ||||
|  | ||||
| <p>Authenticated users allowed from: | ||||
|  | ||||
| {% if "0.0.0.0/0" in session.user_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     </p> | ||||
|     <ul> | ||||
|         {% for i in session.user_subnets %} | ||||
|             <li>{{ i }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|  | ||||
|  | ||||
| <p>Request submission is allowed from: | ||||
|  | ||||
| {% if "0.0.0.0/0" in session.request_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     </p> | ||||
|     <ul> | ||||
|         {% for subnet in session.request_subnets %} | ||||
|             <li>{{ subnet }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|  | ||||
| <p>Autosign is allowed from: | ||||
| {% if "0.0.0.0/0" in session.autosign_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     </p> | ||||
|     <ul> | ||||
|         {% for subnet in session.autosign_subnets %} | ||||
|             <li>{{ subnet }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|  | ||||
| <p>Authority administration is allowed from: | ||||
| {% if "0.0.0.0/0" in session.admin_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     <ul> | ||||
|         {% for subnet in session.admin_subnets %} | ||||
|             <li>{{ subnet }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|  | ||||
| <p>Authority administration allowed for:</p> | ||||
|  | ||||
| <ul> | ||||
| {% for handle, full_name in session.admin_users %} | ||||
|     <li>{{ full_name }}</li> | ||||
| {% endfor %} | ||||
| </ul> | ||||
| </section> | ||||
|  | ||||
| {% 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.requests %} | ||||
|         {% for request in session.authority.requests %} | ||||
|              {% include "views/request.html" %} | ||||
| 	    {% 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> | ||||
|             <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 %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										3
									
								
								certidude/static/views/tags.html
									
									
									
									
									
										Normal 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> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										7
									
								
								certidude/templates/mail/certificate-signed.md
									
									
									
									
									
										Normal 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. | ||||
							
								
								
									
										20
									
								
								certidude/templates/nginx-https-site.conf
									
									
									
									
									
										Normal 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}}; | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								certidude/templates/nginx-tls.conf
									
									
									
									
									
										Normal 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}}; | ||||
|  | ||||
| @@ -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 %} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||