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 | # PyBuilder | ||||||
| target/ | target/ | ||||||
|  |  | ||||||
|  | # npm | ||||||
|  | node_modules/ | ||||||
|  |  | ||||||
|  | # diff | ||||||
|  | *.diff | ||||||
|   | |||||||
| @@ -1,13 +1,11 @@ | |||||||
| include README.rst | include README.rst | ||||||
| include certidude/templates/*.sh |  | ||||||
| include certidude/templates/*.html |  | ||||||
| include certidude/templates/*.svg |  | ||||||
| include certidude/templates/*.ovpn | include certidude/templates/*.ovpn | ||||||
| include certidude/templates/*.cnf |  | ||||||
| include certidude/templates/*.conf | include certidude/templates/*.conf | ||||||
| include certidude/templates/*.ini | include certidude/templates/*.ini | ||||||
|  | include certidude/templates/mail/*.md | ||||||
| include certidude/static/js/*.js | include certidude/static/js/*.js | ||||||
| include certidude/static/css/*.css | include certidude/static/css/*.css | ||||||
| include certidude/static/fonts/*.woff2 | include certidude/static/fonts/*.woff2 | ||||||
| include certidude/static/img/*.svg | include certidude/static/img/*.svg | ||||||
| include certidude/static/*.html | include certidude/static/*.html | ||||||
|  | include certidude/sql/*/*.sql | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						| @@ -67,8 +67,11 @@ To install Certidude: | |||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev libkrb5-dev |     apt-get install -y python python-pip python-dev cython \ | ||||||
|     pip3 install --allow-external mysql-connector-python  mysql-connector-python |         python-pysqlite2 python-mysql.connector python-ldap \ | ||||||
|  |         build-essential libffi-dev libssl-dev libkrb5-dev \ | ||||||
|  |         ldap-utils krb5-user default-mta \ | ||||||
|  |         libsasl2-modules-gssapi-mit | ||||||
|     pip3 install certidude |     pip3 install certidude | ||||||
|  |  | ||||||
| Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, | Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, | ||||||
| @@ -79,6 +82,7 @@ Create a system user for ``certidude``: | |||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     adduser --system --no-create-home --group certidude |     adduser --system --no-create-home --group certidude | ||||||
|  |     mkdir /etc/certidude | ||||||
|  |  | ||||||
|  |  | ||||||
| Setting up CA | Setting up CA | ||||||
| @@ -144,7 +148,7 @@ Install ``nginx`` and ``uwsgi``: | |||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     apt-get install nginx uwsgi uwsgi-plugin-python3 |     apt-get install nginx uwsgi uwsgi-plugin-python | ||||||
|  |  | ||||||
| For easy setup following is reccommended: | For easy setup following is reccommended: | ||||||
|  |  | ||||||
| @@ -162,7 +166,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl | |||||||
|     vaccum = true |     vaccum = true | ||||||
|     uid = certidude |     uid = certidude | ||||||
|     gid = certidude |     gid = certidude | ||||||
|     plugins = python34 |     plugins = python | ||||||
|     chdir = /tmp |     chdir = /tmp | ||||||
|     module = certidude.wsgi |     module = certidude.wsgi | ||||||
|     callable = app |     callable = app | ||||||
| @@ -192,7 +196,7 @@ configure the site in /etc/nginx/sites-available/certidude: | |||||||
|         server_name localhost; |         server_name localhost; | ||||||
|         listen 80 default_server; |         listen 80 default_server; | ||||||
|         listen [::]:80 default_server ipv6only=on; |         listen [::]:80 default_server ipv6only=on; | ||||||
|         root /usr/local/lib/python3.4/dist-packages/certidude/static; |         root /usr/local/lib/python2.7/dist-packages/certidude/static; | ||||||
|  |  | ||||||
|         location /api/ { |         location /api/ { | ||||||
|             include uwsgi_params; |             include uwsgi_params; | ||||||
| @@ -201,19 +205,20 @@ configure the site in /etc/nginx/sites-available/certidude: | |||||||
|  |  | ||||||
|         # Add following three if you wish to enable push server on this machine |         # Add following three if you wish to enable push server on this machine | ||||||
|         location /pub { |         location /pub { | ||||||
|             allow 127.0.0.1; # Allow publishing only from CA machine |             allow 127.0.0.1; | ||||||
|             push_stream_publisher admin; |             nchan_publisher http; | ||||||
|             push_stream_channels_path $arg_id; |             nchan_store_messages off; | ||||||
|  |             nchan_channel_id $arg_id; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         location ~ "^/lp/(.*)" { |         location ~ "^/lp/(.*)" { | ||||||
|             push_stream_channels_path $1; |             nchan_subscriber longpoll; | ||||||
|             push_stream_subscriber long-polling; |             nchan_channel_id $1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         location ~ "^/ev/(.*)" { |         location ~ "^/ev/(.*)" { | ||||||
|             push_stream_channels_path $1; |             nchan_subscriber eventsource; | ||||||
|             push_stream_subscriber eventsource; |             nchan_channel_id $1; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -254,6 +259,8 @@ Also adjust ``/etc/nginx/nginx.conf``: | |||||||
|  |  | ||||||
| In your CA ssl.cnf make sure Certidude is aware of your nginx setup: | In your CA ssl.cnf make sure Certidude is aware of your nginx setup: | ||||||
|  |  | ||||||
|  | .. code:: | ||||||
|  |  | ||||||
|     push_server = http://push.example.com/ |     push_server = http://push.example.com/ | ||||||
|  |  | ||||||
| Restart the services: | Restart the services: | ||||||
| @@ -283,7 +290,7 @@ Make sure Certidude machine's fully qualified hostname is correct in ``/etc/host | |||||||
|     127.0.0.1 localhost |     127.0.0.1 localhost | ||||||
|     127.0.1.1 ca.example.lan ca |     127.0.1.1 ca.example.lan ca | ||||||
|  |  | ||||||
| Set up Samba client configuration in ``/etc/samba/smb.conf``: | Reset Samba client configuration in ``/etc/samba/smb.conf``: | ||||||
|  |  | ||||||
| .. code:: ini | .. code:: ini | ||||||
|  |  | ||||||
| @@ -294,11 +301,36 @@ Set up Samba client configuration in ``/etc/samba/smb.conf``: | |||||||
|     realm = EXAMPLE.LAN |     realm = EXAMPLE.LAN | ||||||
|     kerberos method = system keytab |     kerberos method = system keytab | ||||||
|  |  | ||||||
|  | Reset Kerberos configuration in ``/etc/krb5.conf``: | ||||||
|  |  | ||||||
|  | .. code:: ini | ||||||
|  |  | ||||||
|  |     [libdefaults] | ||||||
|  |     default_realm = EXAMPLE.LAN | ||||||
|  |     dns_lookup_realm = true | ||||||
|  |     dns_lookup_kdc = true | ||||||
|  |     forwardable = true | ||||||
|  |     proxiable = true | ||||||
|  |  | ||||||
|  | Initialize Kerberos credentials: | ||||||
|  |  | ||||||
|  | .. code:: bash | ||||||
|  |  | ||||||
|  |     kinit administrator | ||||||
|  |  | ||||||
|  | Join the machine to domain: | ||||||
|  |  | ||||||
|  | .. code:: bash | ||||||
|  |  | ||||||
|  |     net ads join -k | ||||||
|  |  | ||||||
| Set up Kerberos keytab for the web service: | Set up Kerberos keytab for the web service: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -U Administrator |     KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k | ||||||
|  |     chown root:certidude /etc/certidude/server.keytab | ||||||
|  |     chmod 640 /etc/certidude/server.keytab | ||||||
|  |  | ||||||
|  |  | ||||||
| Setting up authorization | Setting up authorization | ||||||
| @@ -379,22 +411,29 @@ Clone the repository: | |||||||
|     git clone https://github.com/laurivosandi/certidude |     git clone https://github.com/laurivosandi/certidude | ||||||
|     cd certidude |     cd certidude | ||||||
|  |  | ||||||
|  | Install dependencies as shown above and additionally: | ||||||
|  |  | ||||||
|  | .. code:: bash | ||||||
|  |  | ||||||
|  |     pip install -r requirements.txt | ||||||
|  |  | ||||||
| To generate templates: | To generate templates: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     apt-get install npm nodejs |     apt-get install npm nodejs | ||||||
|     npm install nunjucks |     sudo ln -s nodejs /usr/bin/node # Fix 'env node' on Ubuntu 14.04 | ||||||
|     nunjucks-precompile --include "\\.html$" --include "\\.svg" certidude/static/ > certidude/static/js/templates.js |     npm install -g nunjucks | ||||||
|  |     nunjucks-precompile --include "\\.html$" --include "\\.svg$" certidude/static/ > certidude/static/js/templates.js | ||||||
|  |  | ||||||
| To run from source tree: | To run from source tree: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python3 misc/certidude |     PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude | ||||||
|  |  | ||||||
| To install the package from the source: | To install the package from the source: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     python3 setup.py  install --single-version-externally-managed --root / |     python setup.py  install --single-version-externally-managed --root / | ||||||
|   | |||||||
| @@ -1,13 +1,19 @@ | |||||||
|  | # encoding: utf-8 | ||||||
|  |  | ||||||
| import falcon | import falcon | ||||||
| import mimetypes | import mimetypes | ||||||
|  | import logging | ||||||
| import os | import os | ||||||
| import click | import click | ||||||
|  | from datetime import datetime | ||||||
| from time import sleep | from time import sleep | ||||||
| from certidude import authority | from certidude import authority, mailer | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize, event_source | from certidude.decorators import serialize, event_source, csrf_protection | ||||||
| from certidude.wrappers import Request, Certificate | from certidude.wrappers import Request, Certificate | ||||||
| from certidude import config | from certidude import constants, config | ||||||
|  |  | ||||||
|  | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| class CertificateStatusResource(object): | class CertificateStatusResource(object): | ||||||
|     """ |     """ | ||||||
| @@ -24,7 +30,9 @@ class CertificateStatusResource(object): | |||||||
|  |  | ||||||
| class CertificateAuthorityResource(object): | class CertificateAuthorityResource(object): | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|  |         logger.info("Served CA certificate to %s", req.context.get("remote_addr")) | ||||||
|         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") |         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") | ||||||
|  |         resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crt") |         resp.append_header("Content-Disposition", "attachment; filename=ca.crt") | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -34,16 +42,54 @@ class SessionResource(object): | |||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     @event_source |     @event_source | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|  |         if config.ACCOUNTS_BACKEND == "ldap": | ||||||
|  |             import ldap | ||||||
|  |             ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*") | ||||||
|  |             r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, | ||||||
|  |                     ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"]) | ||||||
|  |  | ||||||
|  |             for dn,entry in r: | ||||||
|  |                 cn, = entry.get("cn") | ||||||
|  |                 break | ||||||
|  |             else: | ||||||
|  |                 raise ValueError("Failed to look up group %s in LDAP" % repr(group_name)) | ||||||
|  |  | ||||||
|  |             admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")]) | ||||||
|  |         elif config.ACCOUNTS_BACKEND == "posix": | ||||||
|  |             import grp | ||||||
|  |             _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP) | ||||||
|  |             admins = dict([(j, j) for j in members]) | ||||||
|  |         else: | ||||||
|  |             raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) | ||||||
|  |  | ||||||
|         return dict( |         return dict( | ||||||
|             username=req.context.get("user"), |             user = dict( | ||||||
|             event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, |                 name=req.context.get("user").name, | ||||||
|  |                 gn=req.context.get("user").given_name, | ||||||
|  |                 sn=req.context.get("user").surname, | ||||||
|  |                 mail=req.context.get("user").mail | ||||||
|  |             ), | ||||||
|  |             request_submission_allowed = sum( # Dirty hack! | ||||||
|  |                 [req.context.get("remote_addr") in j | ||||||
|  |                     for j in config.REQUEST_SUBNETS]), | ||||||
|  |             user_subnets = config.USER_SUBNETS, | ||||||
|             autosign_subnets = config.AUTOSIGN_SUBNETS, |             autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||||
|             request_subnets = config.REQUEST_SUBNETS, |             request_subnets = config.REQUEST_SUBNETS, | ||||||
|             admin_subnets=config.ADMIN_SUBNETS, |             admin_subnets=config.ADMIN_SUBNETS, | ||||||
|             admin_users=config.ADMIN_USERS, |             admin_users = admins, | ||||||
|  |             #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(), |                 requests=authority.list_requests(), | ||||||
|                 signed=authority.list_signed(), |                 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): | class StaticResource(object): | ||||||
| @@ -58,7 +104,7 @@ class StaticResource(object): | |||||||
|  |  | ||||||
|         if os.path.isdir(path): |         if os.path.isdir(path): | ||||||
|             path = os.path.join(path, "index.html") |             path = os.path.join(path, "index.html") | ||||||
|         print("Serving:", path) |         click.echo("Serving: %s" % path) | ||||||
|  |  | ||||||
|         if os.path.exists(path): |         if os.path.exists(path): | ||||||
|             content_type, content_encoding = mimetypes.guess_type(path) |             content_type, content_encoding = mimetypes.guess_type(path) | ||||||
| @@ -72,7 +118,33 @@ class StaticResource(object): | |||||||
|             resp.body = "File '%s' not found" % req.path |             resp.body = "File '%s' not found" % req.path | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BundleResource(object): | ||||||
|  |     @login_required | ||||||
|  |     def on_get(self, req, resp): | ||||||
|  |         common_name = req.context["user"].mail | ||||||
|  |         logger.info("Signing bundle %s for %s", common_name, req.context.get("user")) | ||||||
|  |         resp.set_header("Content-Type", "application/x-pkcs12") | ||||||
|  |         resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name) | ||||||
|  |         resp.body, cert = authority.generate_pkcs12_bundle(common_name, | ||||||
|  |                                 owner=req.context.get("user")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import ipaddress | ||||||
|  |  | ||||||
|  | class NormalizeMiddleware(object): | ||||||
|  |     @csrf_protection | ||||||
|  |     def process_request(self, req, resp, *args): | ||||||
|  |         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" | ||||||
|  |         req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) | ||||||
|  |  | ||||||
|  |     def process_response(self, req, resp, resource): | ||||||
|  |         # wtf falcon?! | ||||||
|  |         if isinstance(resp.location, unicode): | ||||||
|  |             resp.location = resp.location.encode("ascii") | ||||||
|  |  | ||||||
| def certidude_app(): | def certidude_app(): | ||||||
|  |     from certidude import config | ||||||
|  |  | ||||||
|     from .revoked import RevocationListResource |     from .revoked import RevocationListResource | ||||||
|     from .signed import SignedCertificateListResource, SignedCertificateDetailResource |     from .signed import SignedCertificateListResource, SignedCertificateDetailResource | ||||||
|     from .request import RequestListResource, RequestDetailResource |     from .request import RequestListResource, RequestDetailResource | ||||||
| @@ -82,60 +154,56 @@ def certidude_app(): | |||||||
|     from .tag import TagResource, TagDetailResource |     from .tag import TagResource, TagDetailResource | ||||||
|     from .cfg import ConfigResource, ScriptResource |     from .cfg import ConfigResource, ScriptResource | ||||||
|  |  | ||||||
|     app = falcon.API() |     app = falcon.API(middleware=NormalizeMiddleware()) | ||||||
|  |  | ||||||
|     # Certificate authority API calls |     # Certificate authority API calls | ||||||
|     app.add_route("/api/ocsp/", CertificateStatusResource()) |     app.add_route("/api/ocsp/", CertificateStatusResource()) | ||||||
|  |     app.add_route("/api/bundle/", BundleResource()) | ||||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) |     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||||
|     app.add_route("/api/revoked/", RevocationListResource()) |     app.add_route("/api/revoked/", RevocationListResource()) | ||||||
|     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) |     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) | ||||||
|     app.add_route("/api/signed/", SignedCertificateListResource()) |     app.add_route("/api/signed/", SignedCertificateListResource()) | ||||||
|     app.add_route("/api/request/{cn}/", RequestDetailResource()) |     app.add_route("/api/request/{cn}/", RequestDetailResource()) | ||||||
|     app.add_route("/api/request/", RequestListResource()) |     app.add_route("/api/request/", RequestListResource()) | ||||||
|     app.add_route("/api/log/", LogResource()) |  | ||||||
|     app.add_route("/api/tag/", TagResource()) |  | ||||||
|     app.add_route("/api/tag/{identifier}/", TagDetailResource()) |  | ||||||
|     app.add_route("/api/config/", ConfigResource()) |  | ||||||
|     app.add_route("/api/script/", ScriptResource()) |  | ||||||
|     app.add_route("/api/", SessionResource()) |     app.add_route("/api/", SessionResource()) | ||||||
|  |  | ||||||
|     # Gateway API calls, should this be moved to separate project? |     # Gateway API calls, should this be moved to separate project? | ||||||
|     app.add_route("/api/lease/", LeaseResource()) |     app.add_route("/api/lease/", LeaseResource()) | ||||||
|     app.add_route("/api/whois/", WhoisResource()) |     app.add_route("/api/whois/", WhoisResource()) | ||||||
|  |  | ||||||
|     """ |     log_handlers = [] | ||||||
|     Set up logging |     if config.LOGGING_BACKEND == "sql": | ||||||
|     """ |         from certidude.mysqllog import LogHandler | ||||||
|  |         uri = config.cp.get("logging", "database") | ||||||
|  |         log_handlers.append(LogHandler(uri)) | ||||||
|  |         app.add_route("/api/log/", LogResource(uri)) | ||||||
|  |     elif config.LOGGING_BACKEND == "syslog": | ||||||
|  |         from logging.handlers import SyslogHandler | ||||||
|  |         log_handlers.append(SysLogHandler()) | ||||||
|  |         # Browsing syslog via HTTP is obviously not possible out of the box | ||||||
|  |     elif config.LOGGING_BACKEND: | ||||||
|  |         raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) | ||||||
|  |  | ||||||
|     from certidude import config |     if config.TAGGING_BACKEND == "sql": | ||||||
|     from certidude.mysqllog import MySQLLogHandler |         uri = config.cp.get("tagging", "database") | ||||||
|     from datetime import datetime |         app.add_route("/api/tag/", TagResource(uri)) | ||||||
|     import logging |         app.add_route("/api/tag/{identifier}/", TagDetailResource(uri)) | ||||||
|     import socket |         app.add_route("/api/config/", ConfigResource(uri)) | ||||||
|     import json |         app.add_route("/api/script/", ScriptResource(uri)) | ||||||
|  |     elif config.TAGGING_BACKEND: | ||||||
|  |         raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND) | ||||||
|  |  | ||||||
|  |     if config.PUSH_PUBLISH: | ||||||
|     class PushLogHandler(logging.Handler): |         from certidude.push import PushLogHandler | ||||||
|         def emit(self, record): |         log_handlers.append(PushLogHandler()) | ||||||
|             from certidude.push import publish |  | ||||||
|             publish("log-entry", dict( |  | ||||||
|                 created = datetime.fromtimestamp(record.created), |  | ||||||
|                 message = record.msg % record.args, |  | ||||||
|                 severity = record.levelname.lower())) |  | ||||||
|  |  | ||||||
|     if config.DATABASE_POOL: |  | ||||||
|         sql_handler = MySQLLogHandler(config.DATABASE_POOL) |  | ||||||
|     push_handler = PushLogHandler() |  | ||||||
|  |  | ||||||
|     for facility in "api", "cli": |     for facility in "api", "cli": | ||||||
|         logger = logging.getLogger(facility) |         logger = logging.getLogger(facility) | ||||||
|         logger.setLevel(logging.DEBUG) |         logger.setLevel(logging.DEBUG) | ||||||
|         if config.DATABASE_POOL: |         for handler in log_handlers: | ||||||
|             logger.addHandler(sql_handler) |             logger.addHandler(handler) | ||||||
|         logger.addHandler(push_handler) |  | ||||||
|  |  | ||||||
|  |     logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN) | ||||||
|     logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN) |  | ||||||
|  |  | ||||||
|     import atexit |     import atexit | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ from random import choice | |||||||
| from certidude import config | from certidude import config | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
|  | from certidude.relational import RelationalMixin | ||||||
| from jinja2 import Environment, FileSystemLoader | from jinja2 import Environment, FileSystemLoader | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
| @@ -39,43 +40,42 @@ where | |||||||
|     device.cn = %s |     device.cn = %s | ||||||
| """ | """ | ||||||
|  |  | ||||||
| SQL_SELECT_INHERITANCE = """ |  | ||||||
|  | SQL_SELECT_RULES = """ | ||||||
| select | select | ||||||
|     tag_inheritance.`id` as `id`, |     tag.cn as `cn`, | ||||||
|     tag.id as `tag_id`, |     tag.key as `tag_key`, | ||||||
|     tag.`key` as `match_key`, |     tag.value as `tag_value`, | ||||||
|     tag.`value` as `match_value`, |     tag_properties.property_key as `property_key`, | ||||||
|     tag_inheritance.`key` as `key`, |     tag_properties.property_value as `property_value` | ||||||
|     tag_inheritance.`value` as `value` | from | ||||||
| from tag_inheritance |     tag_properties | ||||||
| join tag on tag.id = tag_inheritance.tag_id | join | ||||||
|  |     tag | ||||||
|  | on | ||||||
|  |     tag.key = tag_properties.tag_key and | ||||||
|  |     tag.value = tag_properties.tag_value | ||||||
| """ | """ | ||||||
|  |  | ||||||
| class ConfigResource(object): |  | ||||||
|  | class ConfigResource(RelationalMixin): | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         return self.iterfetch(SQL_SELECT_RULES) | ||||||
|         cursor = conn.cursor(dictionary=True) |  | ||||||
|         cursor.execute(SQL_SELECT_INHERITANCE) |  | ||||||
|         def g(): |  | ||||||
|             for row in cursor: |  | ||||||
|                 yield row |  | ||||||
|             cursor.close() |  | ||||||
|             conn.close() |  | ||||||
|         return g() |  | ||||||
|  |  | ||||||
| class ScriptResource(object): |  | ||||||
|  | class ScriptResource(RelationalMixin): | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         from certidude.api.whois import address_to_identity |         from certidude.api.whois import address_to_identity | ||||||
|  |  | ||||||
|         node = address_to_identity( |         node = address_to_identity( | ||||||
|             config.DATABASE_POOL.get_connection(), |             self.connect(), | ||||||
|             ipaddress.ip_address(req.env["REMOTE_ADDR"]) |             req.context.get("remote_addr") | ||||||
|         ) |         ) | ||||||
|         if not node: |         if not node: | ||||||
|             resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"] |             resp.body = "Could not map IP address: %s" % req.context.get("remote_addr") | ||||||
|             resp.status = falcon.HTTP_404 |             resp.status = falcon.HTTP_404 | ||||||
|             return |             return | ||||||
|  |  | ||||||
| @@ -84,7 +84,7 @@ class ScriptResource(object): | |||||||
|         key, common_name = identity.split("=") |         key, common_name = identity.split("=") | ||||||
|         assert "=" not in common_name |         assert "=" not in common_name | ||||||
|  |  | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         conn = self.connect() | ||||||
|         cursor = conn.cursor() |         cursor = conn.cursor() | ||||||
|  |  | ||||||
|         resp.set_header("Content-Type", "text/x-shellscript") |         resp.set_header("Content-Type", "text/x-shellscript") | ||||||
|   | |||||||
| @@ -2,38 +2,14 @@ | |||||||
| from certidude import config | from certidude import config | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
|  | from certidude.relational import RelationalMixin | ||||||
|  |  | ||||||
|  | class LogResource(RelationalMixin): | ||||||
|  |     SQL_CREATE_TABLES = "log_tables.sql" | ||||||
|  |  | ||||||
| class LogResource(object): |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         """ |         # TODO: Add last id parameter | ||||||
|         Translate currently online client's IP-address to distinguished name |         return self.iterfetch("select * from log order by created desc") | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         SQL_LOG_ENTRIES = """ |  | ||||||
|             SELECT |  | ||||||
|                 * |  | ||||||
|             FROM |  | ||||||
|                 log |  | ||||||
|             ORDER BY created DESC |  | ||||||
|         """ |  | ||||||
|         conn = config.DATABASE_POOL.get_connection() |  | ||||||
|         cursor = conn.cursor(dictionary=True) |  | ||||||
|         cursor.execute(SQL_LOG_ENTRIES) |  | ||||||
|  |  | ||||||
|         def g(): |  | ||||||
|             for row in cursor: |  | ||||||
|                 yield row |  | ||||||
|             cursor.close() |  | ||||||
|             conn.close() |  | ||||||
|         return tuple(g()) |  | ||||||
|  |  | ||||||
| #        for acquired, released, identity in cursor: |  | ||||||
| #            return { |  | ||||||
| #                "acquired": datetime.utcfromtimestamp(acquired), |  | ||||||
| #                "identity": parse_dn(bytes(identity)) |  | ||||||
| #            } |  | ||||||
| #        return None |  | ||||||
|          |  | ||||||
|   | |||||||
| @@ -5,45 +5,42 @@ import logging | |||||||
| import ipaddress | import ipaddress | ||||||
| import os | import os | ||||||
| from certidude import config, authority, helpers, push, errors | from certidude import config, authority, helpers, push, errors | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, login_optional, authorize_admin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
| from certidude.wrappers import Request, Certificate | from certidude.wrappers import Request, Certificate | ||||||
|  | from certidude.firewall import whitelist_subnets, whitelist_content_types | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| class RequestListResource(object): | class RequestListResource(object): | ||||||
|     @serialize |     @serialize | ||||||
|  |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         return helpers.list_requests() |         return authority.list_requests() | ||||||
|  |  | ||||||
|  |     @login_optional | ||||||
|  |     @whitelist_subnets(config.REQUEST_SUBNETS) | ||||||
|  |     @whitelist_content_types("application/pkcs10") | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp): | ||||||
|         """ |         """ | ||||||
|         Submit certificate signing request (CSR) in PEM format |         Submit certificate signing request (CSR) in PEM format | ||||||
|         """ |         """ | ||||||
|         # Parse remote IPv4/IPv6 address |  | ||||||
|         remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) |  | ||||||
|  |  | ||||||
|         # Check for CSR submission whitelist |         body = req.stream.read(req.content_length) | ||||||
|         if config.REQUEST_SUBNETS: |  | ||||||
|             for subnet in config.REQUEST_SUBNETS: |  | ||||||
|                 if subnet.overlaps(remote_addr): |  | ||||||
|                     break |  | ||||||
|             else: |  | ||||||
|                logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr) |  | ||||||
|                raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr) |  | ||||||
|  |  | ||||||
|         if req.get_header("Content-Type") != "application/pkcs10": |  | ||||||
|             raise falcon.HTTPUnsupportedMediaType( |  | ||||||
|                 "This API call accepts only application/pkcs10 content type") |  | ||||||
|  |  | ||||||
|         body = req.stream.read(req.content_length).decode("ascii") |  | ||||||
|         csr = Request(body) |         csr = Request(body) | ||||||
|  |  | ||||||
|  |         if not csr.common_name: | ||||||
|  |             logger.warning("Rejected signing request without common name from %s", | ||||||
|  |                 req.context.get("remote_addr")) | ||||||
|  |             raise falcon.HTTPBadRequest( | ||||||
|  |                 "Bad request", | ||||||
|  |                 "No common name specified!") | ||||||
|  |  | ||||||
|         # Check if this request has been already signed and return corresponding certificte if it has been signed |         # Check if this request has been already signed and return corresponding certificte if it has been signed | ||||||
|         try: |         try: | ||||||
|             cert = authority.get_signed(csr.common_name) |             cert = authority.get_signed(csr.common_name) | ||||||
|         except FileNotFoundError: |         except EnvironmentError: | ||||||
|             pass |             pass | ||||||
|         else: |         else: | ||||||
|             if cert.pubkey == csr.pubkey: |             if cert.pubkey == csr.pubkey: | ||||||
| @@ -56,12 +53,12 @@ class RequestListResource(object): | |||||||
|         # Process automatic signing if the IP address is whitelisted and autosigning was requested |         # Process automatic signing if the IP address is whitelisted and autosigning was requested | ||||||
|         if req.get_param_as_bool("autosign"): |         if req.get_param_as_bool("autosign"): | ||||||
|             for subnet in config.AUTOSIGN_SUBNETS: |             for subnet in config.AUTOSIGN_SUBNETS: | ||||||
|                 if subnet.overlaps(remote_addr): |                 if subnet.overlaps(req.context.get("remote_addr")): | ||||||
|                     try: |                     try: | ||||||
|                         resp.set_header("Content-Type", "application/x-x509-user-cert") |                         resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||||
|                         resp.body = authority.sign(csr).dump() |                         resp.body = authority.sign(csr).dump() | ||||||
|                         return |                         return | ||||||
|                     except FileExistsError: # Certificate already exists, try to save the request |                     except EnvironmentError: # Certificate already exists, try to save the request | ||||||
|                         pass |                         pass | ||||||
|                     break |                     break | ||||||
|  |  | ||||||
| @@ -73,7 +70,8 @@ class RequestListResource(object): | |||||||
|             pass |             pass | ||||||
|         except errors.DuplicateCommonNameError: |         except errors.DuplicateCommonNameError: | ||||||
|             # TODO: Certificate renewal |             # TODO: Certificate renewal | ||||||
|             logger.warning("Rejected signing request with overlapping common name from %s", req.env["REMOTE_ADDR"]) |             logger.warning("Rejected signing request with overlapping common name from %s", | ||||||
|  |                 req.context.get("remote_addr")) | ||||||
|             raise falcon.HTTPConflict( |             raise falcon.HTTPConflict( | ||||||
|                 "CSR with such CN already exists", |                 "CSR with such CN already exists", | ||||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") |                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||||
| @@ -86,12 +84,12 @@ class RequestListResource(object): | |||||||
|             url = config.PUSH_LONG_POLL % csr.fingerprint() |             url = config.PUSH_LONG_POLL % csr.fingerprint() | ||||||
|             click.echo("Redirecting to: %s"  % url) |             click.echo("Redirecting to: %s"  % url) | ||||||
|             resp.status = falcon.HTTP_SEE_OTHER |             resp.status = falcon.HTTP_SEE_OTHER | ||||||
|             resp.set_header("Location", url) |             resp.set_header("Location", url.encode("ascii")) | ||||||
|             logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url) |             logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) | ||||||
|         else: |         else: | ||||||
|             # Request was accepted, but not processed |             # Request was accepted, but not processed | ||||||
|             resp.status = falcon.HTTP_202 |             resp.status = falcon.HTTP_202 | ||||||
|             logger.info("Signing request from %s stored", req.env["REMOTE_ADDR"]) |             logger.info("Signing request from %s stored", req.context.get("remote_addr")) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestDetailResource(object): | class RequestDetailResource(object): | ||||||
| @@ -101,11 +99,8 @@ class RequestDetailResource(object): | |||||||
|         Fetch certificate signing request as PEM |         Fetch certificate signing request as PEM | ||||||
|         """ |         """ | ||||||
|         csr = authority.get_request(cn) |         csr = authority.get_request(cn) | ||||||
| #        if not os.path.exists(path): |         logger.debug("Signing request %s was downloaded by %s", | ||||||
| #            raise falcon.HTTPNotFound() |             csr.common_name, req.context.get("remote_addr")) | ||||||
|  |  | ||||||
|         resp.set_header("Content-Type", "application/pkcs10") |  | ||||||
|         resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name) |  | ||||||
|         return csr |         return csr | ||||||
|  |  | ||||||
|     @login_required |     @login_required | ||||||
| @@ -120,14 +115,17 @@ class RequestDetailResource(object): | |||||||
|         resp.body = "Certificate successfully signed" |         resp.body = "Certificate successfully signed" | ||||||
|         resp.status = falcon.HTTP_201 |         resp.status = falcon.HTTP_201 | ||||||
|         resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) |         resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) | ||||||
|         logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context["user"], req.env["REMOTE_ADDR"]) |         logger.info("Signing request %s signed by %s from %s", csr.common_name, | ||||||
|  |             req.context.get("user"), req.context.get("remote_addr")) | ||||||
|  |  | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, cn): |     def on_delete(self, req, resp, cn): | ||||||
|         try: |         try: | ||||||
|             authority.delete_request(cn) |             authority.delete_request(cn) | ||||||
|         except FileNotFoundError: |             # Logging implemented in the function above | ||||||
|  |         except EnvironmentError as e: | ||||||
|             resp.body = "No certificate CN=%s found" % cn |             resp.body = "No certificate CN=%s found" % cn | ||||||
|             logger.warning("User %s attempted to delete non-existant signing request %s from %s", req.context["user"], cn, req.env["REMOTE_ADDR"]) |             logger.warning("User %s failed to delete signing request %s from %s, reason: %s", | ||||||
|  |                 req.context["user"], cn, req.context.get("remote_addr"), e) | ||||||
|             raise falcon.HTTPNotFound() |             raise falcon.HTTPNotFound() | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
|  |  | ||||||
|  | import logging | ||||||
| from certidude.authority import export_crl | from certidude.authority import export_crl | ||||||
|  |  | ||||||
|  | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| class RevocationListResource(object): | class RevocationListResource(object): | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|  |         logger.debug("Revocation list requested by %s", req.context.get("remote_addr")) | ||||||
|         resp.set_header("Content-Type", "application/x-pkcs7-crl") |         resp.set_header("Content-Type", "application/x-pkcs7-crl") | ||||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crl") |         resp.append_header("Content-Disposition", "attachment; filename=ca.crl") | ||||||
|         resp.body = export_crl() |         resp.body = export_crl() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,40 +9,35 @@ logger = logging.getLogger("api") | |||||||
|  |  | ||||||
| class SignedCertificateListResource(object): | class SignedCertificateListResource(object): | ||||||
|     @serialize |     @serialize | ||||||
|  |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         for j in authority.list_signed(): |         return {"signed":authority.list_signed()} | ||||||
|             yield omit( |  | ||||||
|                 key_type=j.key_type, |  | ||||||
|                 key_length=j.key_length, |  | ||||||
|                 identity=j.identity, |  | ||||||
|                 cn=j.common_name, |  | ||||||
|                 c=j.country_code, |  | ||||||
|                 st=j.state_or_county, |  | ||||||
|                 l=j.city, |  | ||||||
|                 o=j.organization, |  | ||||||
|                 ou=j.organizational_unit, |  | ||||||
|                 fingerprint=j.fingerprint()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SignedCertificateDetailResource(object): | class SignedCertificateDetailResource(object): | ||||||
|     @serialize |     @serialize | ||||||
|     def on_get(self, req, resp, cn): |     def on_get(self, req, resp, cn): | ||||||
|         # Compensate for NTP lag |         # Compensate for NTP lag | ||||||
|         from time import sleep | #        from time import sleep | ||||||
|         sleep(5) | #        sleep(5) | ||||||
|         try: |         try: | ||||||
|             logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"]) |             cert = authority.get_signed(cn) | ||||||
|             resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn) |         except EnvironmentError: | ||||||
|             return authority.get_signed(cn) |             logger.warning("Failed to serve non-existant certificate %s to %s", | ||||||
|         except FileNotFoundError: |                 cn, req.context.get("remote_addr")) | ||||||
|             logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"]) |  | ||||||
|             resp.body = "No certificate CN=%s found" % cn |             resp.body = "No certificate CN=%s found" % cn | ||||||
|             raise falcon.HTTPNotFound() |             raise falcon.HTTPNotFound() | ||||||
|  |         else: | ||||||
|  |             logger.debug("Served certificate %s to %s", | ||||||
|  |                 cn, req.context.get("remote_addr")) | ||||||
|  |             return cert | ||||||
|  |  | ||||||
|  |  | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, cn): |     def on_delete(self, req, resp, cn): | ||||||
|         logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"]) |         logger.info("Revoked certificate %s by %s from %s", | ||||||
|  |             cn, req.context.get("user"), req.context.get("remote_addr")) | ||||||
|         authority.revoke_certificate(cn) |         authority.revoke_certificate(cn) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,117 +1,63 @@ | |||||||
|  |  | ||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| from certidude import config | from certidude.relational import RelationalMixin | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| SQL_TAG_LIST = """ | class TagResource(RelationalMixin): | ||||||
| select |     SQL_CREATE_TABLES = "tag_tables.sql" | ||||||
|     device_tag.id as `id`, |  | ||||||
| 	tag.key as `key`, |  | ||||||
| 	tag.value as `value`, |  | ||||||
| 	device.cn as `cn` |  | ||||||
| from |  | ||||||
| 	device_tag |  | ||||||
| join |  | ||||||
| 	tag |  | ||||||
| on |  | ||||||
| 	device_tag.tag_id = tag.id |  | ||||||
| join |  | ||||||
| 	device |  | ||||||
| on |  | ||||||
| 	device_tag.device_id = device.id |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s" |  | ||||||
|  |  | ||||||
| class TagResource(object): |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         return self.iterfetch("select * from tag") | ||||||
|         cursor = conn.cursor(dictionary=True) |  | ||||||
|         cursor.execute(SQL_TAG_LIST) |  | ||||||
|  |  | ||||||
|         def g(): |  | ||||||
|             for row in cursor: |  | ||||||
|                 yield row |  | ||||||
|             cursor.close() |  | ||||||
|             conn.close() |  | ||||||
|         return tuple(g()) |  | ||||||
|  |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp): | ||||||
|         from certidude import push |         from certidude import push | ||||||
|         conn = config.DATABASE_POOL.get_connection() |  | ||||||
|         cursor = conn.cursor() |  | ||||||
|  |  | ||||||
|         args = req.get_param("cn"), |  | ||||||
|         cursor.execute( |  | ||||||
|             "insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args) |  | ||||||
|         device_id = cursor.lastrowid |  | ||||||
|  |  | ||||||
|         args = req.get_param("key"), req.get_param("value") |  | ||||||
|         cursor.execute( |  | ||||||
|             "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) |  | ||||||
|         tag_id = cursor.lastrowid |  | ||||||
|  |  | ||||||
|         args = device_id, tag_id |  | ||||||
|         cursor.execute( |  | ||||||
|             "insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args) |  | ||||||
|  |  | ||||||
|         push.publish("tag-added", str(cursor.lastrowid)) |  | ||||||
|  |  | ||||||
|         args = req.get_param("cn"), req.get_param("key"), req.get_param("value") |         args = req.get_param("cn"), req.get_param("key"), req.get_param("value") | ||||||
|  |         rowid = self.sql_execute("tag_insert.sql", *args) | ||||||
|  |         push.publish("tag-added", str(rowid)) | ||||||
|         logger.debug("Tag cn=%s, key=%s, value=%s added" % args) |         logger.debug("Tag cn=%s, key=%s, value=%s added" % args) | ||||||
|         conn.commit() |  | ||||||
|         cursor.close() |  | ||||||
|         conn.close() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TagDetailResource(object): | class TagDetailResource(RelationalMixin): | ||||||
|  |     SQL_CREATE_TABLES = "tag_tables.sql" | ||||||
|  |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp, identifier): |     def on_get(self, req, resp, identifier): | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         conn = self.sql_connect() | ||||||
|         cursor = conn.cursor(dictionary=True) |         cursor = conn.cursor() | ||||||
|         cursor.execute(SQL_TAG_DETAIL, (identifier,)) |         if self.uri.scheme == "mysql": | ||||||
|  |             cursor.execute("select `cn`, `key`, `value` from tag where id = %s", (identifier,)) | ||||||
|  |         else: | ||||||
|  |             cursor.execute("select `cn`, `key`, `value` from tag where id = ?", (identifier,)) | ||||||
|  |         cols = [j[0] for j in cursor.description] | ||||||
|         for row in cursor: |         for row in cursor: | ||||||
|             cursor.close() |             cursor.close() | ||||||
|             conn.close() |             conn.close() | ||||||
|             return row |             return dict(zip(cols, row)) | ||||||
|         cursor.close() |         cursor.close() | ||||||
|         conn.close() |         conn.close() | ||||||
|         raise falcon.HTTPNotFound() |         raise falcon.HTTPNotFound() | ||||||
|  |  | ||||||
|  |  | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_put(self, req, resp, identifier): |     def on_put(self, req, resp, identifier): | ||||||
|         from certidude import push |         from certidude import push | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         args = req.get_param("value"), identifier | ||||||
|         cursor = conn.cursor() |         self.sql_execute("tag_update.sql", *args) | ||||||
|  |  | ||||||
|         # Create tag if necessary |  | ||||||
|         args = req.get_param("key"), req.get_param("value") |  | ||||||
|         cursor.execute( |  | ||||||
|             "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) |  | ||||||
|         tag_id = cursor.lastrowid |  | ||||||
|  |  | ||||||
|         # Attach tag to device |  | ||||||
|         cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1", |  | ||||||
|             (tag_id, identifier)) |  | ||||||
|         conn.commit() |  | ||||||
|  |  | ||||||
|         cursor.close() |  | ||||||
|         conn.close() |  | ||||||
|  |  | ||||||
|         logger.debug("Tag %s updated, value set to %s", |         logger.debug("Tag %s updated, value set to %s", | ||||||
|             identifier, req.get_param("value")) |             identifier, req.get_param("value")) | ||||||
|         push.publish("tag-updated", identifier) |         push.publish("tag-updated", identifier) | ||||||
| @@ -122,13 +68,6 @@ class TagDetailResource(object): | |||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, identifier): |     def on_delete(self, req, resp, identifier): | ||||||
|         from certidude import push |         from certidude import push | ||||||
|         conn = config.DATABASE_POOL.get_connection() |         self.sql_execute("tag_delete.sql", identifier) | ||||||
|         cursor = conn.cursor() |  | ||||||
|         cursor.execute("delete from device_tag where id = %s", (identifier,)) |  | ||||||
|         conn.commit() |  | ||||||
|         cursor.close() |  | ||||||
|         conn.close() |  | ||||||
|         push.publish("tag-removed", identifier) |         push.publish("tag-removed", identifier) | ||||||
|         logger.debug("Tag %s removed" % identifier) |         logger.debug("Tag %s removed" % identifier) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ class WhoisResource(object): | |||||||
|  |  | ||||||
|         identity = address_to_identity( |         identity = address_to_identity( | ||||||
|             conn, |             conn, | ||||||
|             ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) |             req.context.get("remote_addr") | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         conn.close() |         conn.close() | ||||||
| @@ -55,4 +55,4 @@ class WhoisResource(object): | |||||||
|             return dict(address=identity[0], acquired=identity[1], identity=identity[2]) |             return dict(address=identity[0], acquired=identity[1], identity=identity[2]) | ||||||
|         else: |         else: | ||||||
|             resp.status = falcon.HTTP_403 |             resp.status = falcon.HTTP_403 | ||||||
|             resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"] |             resp.body = "Failed to look up node %s" % req.context.get("remote_addr") | ||||||
|   | |||||||
| @@ -1,108 +1,213 @@ | |||||||
|  |  | ||||||
| import click | import click | ||||||
| import falcon | import falcon | ||||||
| import ipaddress |  | ||||||
| import kerberos | import kerberos | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import socket | import socket | ||||||
| from certidude import config | from certidude.firewall import whitelist_subnets | ||||||
|  | from certidude import config, constants | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| # Vanilla Kerberos provides only username. |  | ||||||
| # AD also embeds PAC (Privilege Attribute Certificate), which |  | ||||||
| # is supposed to be sent via HTTP headers and it contains |  | ||||||
| # the groups user is part of. |  | ||||||
| # Even then we would have to manually look up the e-mail |  | ||||||
| # address eg via LDAP, hence to keep things simple |  | ||||||
| # we simply use Kerberos to authenticate. |  | ||||||
|  |  | ||||||
| FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] | FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] | ||||||
|  |  | ||||||
| if config.AUTHENTICATION_BACKEND == "kerberos": | if config.AUTHENTICATION_BACKENDS == {"kerberos"}: | ||||||
|     if not os.getenv("KRB5_KTNAME"): |     ktname = os.getenv("KRB5_KTNAME") | ||||||
|  |  | ||||||
|  |     if not ktname: | ||||||
|         click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) |         click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) | ||||||
|         exit(250) |         exit(250) | ||||||
|  |     if not os.path.exists(ktname): | ||||||
|  |         click.echo("Kerberos keytab %s does not exist" % ktname, err=True) | ||||||
|  |         exit(248) | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) |         principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) | ||||||
|     except kerberos.KrbError as exc: |     except kerberos.KrbError as exc: | ||||||
|         click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) |         click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True) | ||||||
|         exit(249) |         exit(249) | ||||||
|     else: |     else: | ||||||
|         click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) |         click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class User(object): | ||||||
|  |     def __init__(self, name): | ||||||
|  |         if "@" in name: | ||||||
|  |             self.mail = name | ||||||
|  |             self.name, self.domain = name.split("@") | ||||||
|         else: |         else: | ||||||
|     NotImplemented |             self.mail = None | ||||||
|  |             self.name, self.domain = name, None | ||||||
|  |         self.given_name, self.surname = None, None | ||||||
|  |  | ||||||
| def login_required(func): |     def __repr__(self): | ||||||
|     def pam_authenticate(resource, req, resp, *args, **kwargs): |         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 "): |     def wrapper(func): | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization) |         def posix_check_group_membership(resource, req, resp, *args, **kwargs): | ||||||
|  |             import grp | ||||||
|         from base64 import b64decode |             _, _, gid, members = grp.getgrnam(group_name) | ||||||
|         basic, token = authorization.split(" ", 1) |             if req.context.get("user").name not in members: | ||||||
|         user, passwd = b64decode(token).split(":", 1) |                 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") | ||||||
|         import simplepam |             req.context.get("groups").add(group_name) | ||||||
|         if not simplepam.authenticate(user, passwd, "sshd"): |  | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Invalid password") |  | ||||||
|  |  | ||||||
|         req.context["user"] = user |  | ||||||
|             return func(resource, req, resp, *args, **kwargs) |             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): |         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") |                 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"]) |                 logger.debug("No Kerberos ticket offered while attempting to access %s from %s", | ||||||
|             raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") |                     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: |             try: | ||||||
|                 result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) |                 result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) | ||||||
|             except kerberos.GSSError as ex: |             except kerberos.GSSError as ex: | ||||||
|                 # TODO: logger.error |                 # 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: |             try: | ||||||
|                 result = kerberos.authGSSServerStep(context, token) |                 result = kerberos.authGSSServerStep(context, token) | ||||||
|             except kerberos.GSSError as ex: |             except kerberos.GSSError as ex: | ||||||
|             s = str(dir(ex)) |  | ||||||
|                 kerberos.authGSSServerClean(context) |                 kerberos.authGSSServerClean(context) | ||||||
|                 # TODO: logger.error |                 # 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: |             except kerberos.KrbError as ex: | ||||||
|                 kerberos.authGSSServerClean(context) |                 kerberos.authGSSServerClean(context) | ||||||
|                 # TODO: logger.error |                 # 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) |             user = kerberos.authGSSServerUserName(context) | ||||||
|         req.context["user"], req.context["user_realm"] = user.split("@") |             req.context["user"] = User(user) | ||||||
|  |             req.context["groups"] = set() | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|             # BUGBUG: https://github.com/02strich/pykerberos/issues/6 |                 kerberos.authGSSServerClean(context) | ||||||
|             #kerberos.authGSSServerClean(context) |  | ||||||
|             pass |  | ||||||
|             except kerberos.GSSError as ex: |             except kerberos.GSSError as ex: | ||||||
|                 # TODO: logger.error |                 # 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: |             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"]) |                 logger.debug("Succesfully authenticated user %s for %s from %s", | ||||||
|             return func(resource, req, resp, *args, **kwargs) |                     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: |             elif result == kerberos.AUTH_GSS_CONTINUE: | ||||||
|                 # TODO: logger.error |                 # TODO: logger.error | ||||||
|                 raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") |                 raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") | ||||||
| @@ -110,35 +215,110 @@ def login_required(func): | |||||||
|                 # TODO: logger.error |                 # TODO: logger.error | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") |                 raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") | ||||||
|  |  | ||||||
|     if config.AUTHENTICATION_BACKEND == "kerberos": |  | ||||||
|         return kerberos_authenticate |         def ldap_authenticate(resource, req, resp, *args, **kwargs): | ||||||
|     elif config.AUTHENTICATION_BACKEND == "pam": |             """ | ||||||
|         return pam_authenticate |             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: |                 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 authorize_admin(func): | ||||||
|     def wrapped(self, req, resp, *args, **kwargs): |  | ||||||
|         from certidude import config |  | ||||||
|         # Parse remote IPv4/IPv6 address |  | ||||||
|         remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) |  | ||||||
|  |  | ||||||
|         # Check for administration subnet whitelist |  | ||||||
|         print("Comparing:", config.ADMIN_SUBNETS, "To:", remote_addr) |  | ||||||
|         for subnet in config.ADMIN_SUBNETS: |  | ||||||
|             if subnet.overlaps(remote_addr): |  | ||||||
|                 break |  | ||||||
|         else: |  | ||||||
|             logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) |  | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) |  | ||||||
|  |  | ||||||
|  |     def whitelist_authorize(resource, req, resp, *args, **kwargs): | ||||||
|         # Check for username whitelist |         # Check for username whitelist | ||||||
|         if req.context.get("user") not in config.ADMIN_USERS: |         if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST: | ||||||
|             logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) |             logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", | ||||||
|  |                 req.env["PATH_INFO"], req.context.get("user"), req.context.get("remote_addr")) | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) |             raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) | ||||||
|  |         return func(resource, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|         # Retain username, TODO: Better abstraction with username, e-mail, sn, gn? |     if config.AUTHORIZATION_BACKEND == "whitelist": | ||||||
|  |         return whitelist_authorize | ||||||
|  |     else: | ||||||
|  |         return member_of(config.ADMINS_GROUP)(func) | ||||||
|  |  | ||||||
|         return func(self, req, resp, *args, **kwargs) |  | ||||||
|     return wrapped |  | ||||||
|   | |||||||
| @@ -5,47 +5,64 @@ import re | |||||||
| import socket | import socket | ||||||
| import requests | import requests | ||||||
| from OpenSSL import crypto | from OpenSSL import crypto | ||||||
| from certidude import config, push | from certidude import config, push, mailer | ||||||
| from certidude.wrappers import Certificate, Request | from certidude.wrappers import Certificate, Request | ||||||
| from certidude.signer import raw_sign | from certidude.signer import raw_sign | ||||||
| from certidude import errors | from certidude import errors | ||||||
|  |  | ||||||
| RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" | RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" | ||||||
|  |  | ||||||
| # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ | # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ | ||||||
| # https://jamielinux.com/docs/openssl-certificate-authority/ | # https://jamielinux.com/docs/openssl-certificate-authority/ | ||||||
| # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py | # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Cache CA certificate | ||||||
|  | certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH)) | ||||||
|  |  | ||||||
| def publish_certificate(func): | def publish_certificate(func): | ||||||
|     # TODO: Implement e-mail and nginx notifications using hooks |     # TODO: Implement e-mail and nginx notifications using hooks | ||||||
|     def wrapped(csr, *args, **kwargs): |     def wrapped(csr, *args, **kwargs): | ||||||
|         cert = func(csr, *args, **kwargs) |         cert = func(csr, *args, **kwargs) | ||||||
|         assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) |         assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) | ||||||
|  |  | ||||||
|  |         if cert.email_address: | ||||||
|  |             mailer.send( | ||||||
|  |                 "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address), | ||||||
|  |                 "certificate-signed.md", | ||||||
|  |                 attachments=(cert,), | ||||||
|  |                 certificate=cert) | ||||||
|  |  | ||||||
|         if config.PUSH_PUBLISH: |         if config.PUSH_PUBLISH: | ||||||
|             url = config.PUSH_PUBLISH % csr.fingerprint() |             url = config.PUSH_PUBLISH % csr.fingerprint() | ||||||
|             click.echo("Publishing certificate at %s ..." % url) |             click.echo("Publishing certificate at %s ..." % url) | ||||||
|             requests.post(url, data=cert.dump(), |             requests.post(url, data=cert.dump(), | ||||||
|                 headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) |                 headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) | ||||||
|  |  | ||||||
|  |             # For deleting request in the web view, use pubkey modulo | ||||||
|             push.publish("request-signed", csr.common_name) |             push.publish("request-signed", csr.common_name) | ||||||
|         return cert |         return cert | ||||||
|     return wrapped |     return wrapped | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_request(common_name): | def get_request(common_name): | ||||||
|     if not re.match(RE_HOSTNAME, common_name): |     if not re.match(RE_HOSTNAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name %s" % repr(common_name)) | ||||||
|     return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem"))) |     return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem"))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_signed(common_name): | def get_signed(common_name): | ||||||
|     if not re.match(RE_HOSTNAME, common_name): |     if not re.match(RE_HOSTNAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name %s" % repr(common_name)) | ||||||
|     return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) |     return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_revoked(common_name): | def get_revoked(common_name): | ||||||
|     if not re.match(RE_HOSTNAME, common_name): |     if not re.match(RE_HOSTNAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name %s" % repr(common_name)) | ||||||
|     return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) |     return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def store_request(buf, overwrite=False): | def store_request(buf, overwrite=False): | ||||||
|     """ |     """ | ||||||
|     Store CSR for later processing |     Store CSR for later processing | ||||||
| @@ -92,7 +109,7 @@ def revoke_certificate(common_name): | |||||||
|     cert = get_signed(common_name) |     cert = get_signed(common_name) | ||||||
|     revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) |     revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) | ||||||
|     os.rename(cert.path, revoked_filename) |     os.rename(cert.path, revoked_filename) | ||||||
|     push.publish("certificate-revoked", cert.fingerprint()) |     push.publish("certificate-revoked", cert.common_name) | ||||||
|  |  | ||||||
|  |  | ||||||
| def list_requests(directory=config.REQUESTS_DIR): | def list_requests(directory=config.REQUESTS_DIR): | ||||||
| @@ -136,39 +153,50 @@ def delete_request(common_name): | |||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name") | ||||||
|  |  | ||||||
|     path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") |     path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||||
|     request_sha1sum = Request(open(path)).fingerprint() |     request = Request(open(path)) | ||||||
|     os.unlink(path) |     os.unlink(path) | ||||||
|  |  | ||||||
|     # Publish event at CA channel |     # Publish event at CA channel | ||||||
|     push.publish("request-deleted", request_sha1sum) |     push.publish("request-deleted", request.common_name) | ||||||
|  |  | ||||||
|     # Write empty certificate to long-polling URL |     # Write empty certificate to long-polling URL | ||||||
|     requests.delete(config.PUSH_PUBLISH % request_sha1sum, |     requests.delete(config.PUSH_PUBLISH % request.common_name, | ||||||
|         headers={"User-Agent": "Certidude API"}) |         headers={"User-Agent": "Certidude API"}) | ||||||
|  |  | ||||||
| def generate_p12_bundle(common_name): |  | ||||||
|  | def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): | ||||||
|  |     """ | ||||||
|  |     Generate private key, sign certificate and return PKCS#12 bundle | ||||||
|  |     """ | ||||||
|     # Construct private key |     # Construct private key | ||||||
|     click.echo("Generating 4096-bit RSA key...") |     click.echo("Generating %d-bit RSA key..." % key_size) | ||||||
|     key = crypto.PKey() |     key = crypto.PKey() | ||||||
|     key.generate_key(crypto.TYPE_RSA, 512) |     key.generate_key(crypto.TYPE_RSA, key_size) | ||||||
|  |  | ||||||
|     # Construct CSR |     # Construct CSR | ||||||
|     csr = crypto.X509Req() |     csr = crypto.X509Req() | ||||||
|     csr.set_version(2) # Corresponds to X.509v3 |     csr.set_version(2) # Corresponds to X.509v3 | ||||||
|     csr.set_pubkey(key) |     csr.set_pubkey(key) | ||||||
|     csr.get_subject().CN = common_name |     csr.get_subject().CN = common_name | ||||||
|     buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr).decode("utf-8") |     if owner: | ||||||
|  |         if owner.given_name: | ||||||
|  |             csr.get_subject().GN = owner.given_name | ||||||
|  |         if owner.surname: | ||||||
|  |             csr.get_subject().SN = owner.surname | ||||||
|  |         csr.add_extensions([ | ||||||
|  |             crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)]) | ||||||
|  |  | ||||||
|  |     buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) | ||||||
|  |  | ||||||
|     # Sign CSR |     # Sign CSR | ||||||
|     cert = sign(Request(buf), overwrite=True) |     cert = sign(Request(buf), overwrite=True) | ||||||
|  |  | ||||||
|     # Generate P12 |     # Generate P12 | ||||||
|     ca_certs = crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()), |  | ||||||
|     p12 = crypto.PKCS12() |     p12 = crypto.PKCS12() | ||||||
|     p12.set_privatekey( key ) |     p12.set_privatekey( key ) | ||||||
|     p12.set_certificate( cert._obj ) |     p12.set_certificate( cert._obj ) | ||||||
|     p12.set_ca_certificates( ca_certs ) |     p12.set_ca_certificates([certificate._obj]) | ||||||
|     return p12.export() |     return p12.export(), cert | ||||||
|  |  | ||||||
|  |  | ||||||
| @publish_certificate | @publish_certificate | ||||||
| @@ -187,7 +215,7 @@ def sign(req, overwrite=False, delete=True): | |||||||
|         elif req.pubkey == old_cert.pubkey: |         elif req.pubkey == old_cert.pubkey: | ||||||
|             return old_cert |             return old_cert | ||||||
|         else: |         else: | ||||||
|             raise FileExistsError("Will not overwrite existing certificate") |             raise EnvironmentError("Will not overwrite existing certificate") | ||||||
|  |  | ||||||
|     # Sign via signer process |     # Sign via signer process | ||||||
|     cert_buf = signer_exec("sign-request", req.dump()) |     cert_buf = signer_exec("sign-request", req.dump()) | ||||||
| @@ -216,9 +244,9 @@ def sign2(request, overwrite=False, delete=True, lifetime=None): | |||||||
|     path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem") |     path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem") | ||||||
|     if os.path.exists(path): |     if os.path.exists(path): | ||||||
|         if overwrite: |         if overwrite: | ||||||
|             revoke(request.common_name) |             revoke_certificate(request.common_name) | ||||||
|         else: |         else: | ||||||
|             raise FileExistsError("File %s already exists!" % path) |             raise EnvironmentError("File %s already exists!" % path) | ||||||
|  |  | ||||||
|     buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) |     buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) | ||||||
|     with open(path + ".part", "wb") as fh: |     with open(path + ".part", "wb") as fh: | ||||||
|   | |||||||
							
								
								
									
										199
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						| @@ -3,7 +3,6 @@ | |||||||
|  |  | ||||||
| import asyncore | import asyncore | ||||||
| import click | import click | ||||||
| import configparser |  | ||||||
| import hashlib | import hashlib | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| @@ -14,11 +13,11 @@ import signal | |||||||
| import socket | import socket | ||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
| from certidude.signer import SignServer | from configparser import ConfigParser | ||||||
| from certidude.common import expand_paths | from certidude import constants | ||||||
|  | from certidude.common import expand_paths, ip_address, ip_network | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from humanize import naturaltime | from humanize import naturaltime | ||||||
| from ipaddress import ip_network, ip_address |  | ||||||
| from jinja2 import Environment, PackageLoader | from jinja2 import Environment, PackageLoader | ||||||
| from time import sleep | from time import sleep | ||||||
| from setproctitle import setproctitle | from setproctitle import setproctitle | ||||||
| @@ -66,7 +65,7 @@ if os.getuid() >= 1000: | |||||||
| def certidude_request_spawn(fork): | def certidude_request_spawn(fork): | ||||||
|     from certidude.helpers import certidude_request_certificate |     from certidude.helpers import certidude_request_certificate | ||||||
|  |  | ||||||
|     clients = configparser.ConfigParser() |     clients = ConfigParser() | ||||||
|     clients.readfp(open("/etc/certidude/client.conf")) |     clients.readfp(open("/etc/certidude/client.conf")) | ||||||
|  |  | ||||||
|     services = ConfigParser() |     services = ConfigParser() | ||||||
| @@ -92,7 +91,7 @@ def certidude_request_spawn(fork): | |||||||
|                 os.kill(pid, signal.SIGTERM) |                 os.kill(pid, signal.SIGTERM) | ||||||
|                 click.echo("Terminated process %d" % pid) |                 click.echo("Terminated process %d" % pid) | ||||||
|             os.unlink(pid_path) |             os.unlink(pid_path) | ||||||
|         except (ValueError, ProcessLookupError, FileNotFoundError): |         except EnvironmentError: | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
|         if fork: |         if fork: | ||||||
| @@ -137,7 +136,7 @@ def certidude_request_spawn(fork): | |||||||
|             # Set up IPsec via NetworkManager |             # Set up IPsec via NetworkManager | ||||||
|             if services.get(endpoint, "service") == "network-manager/strongswan": |             if services.get(endpoint, "service") == "network-manager/strongswan": | ||||||
|  |  | ||||||
|                 config = configparser.ConfigParser() |                 config = ConfigParser() | ||||||
|                 config.add_section("connection") |                 config.add_section("connection") | ||||||
|                 config.add_section("vpn") |                 config.add_section("vpn") | ||||||
|                 config.add_section("ipv4") |                 config.add_section("ipv4") | ||||||
| @@ -218,6 +217,7 @@ def certidude_signer_spawn(kill, no_interaction): | |||||||
|     """ |     """ | ||||||
|     Spawn privilege isolated signer process |     Spawn privilege isolated signer process | ||||||
|     """ |     """ | ||||||
|  |     from certidude.signer import SignServer | ||||||
|     from certidude import config |     from certidude import config | ||||||
|  |  | ||||||
|     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") |     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") | ||||||
| @@ -254,7 +254,7 @@ def certidude_signer_spawn(kill, no_interaction): | |||||||
|             pid = int(fh.readline()) |             pid = int(fh.readline()) | ||||||
|             os.kill(pid, 0) |             os.kill(pid, 0) | ||||||
|             click.echo("Found process with PID %d" % pid) |             click.echo("Found process with PID %d" % pid) | ||||||
|     except (ValueError, ProcessLookupError, FileNotFoundError): |     except EnvironmentError: | ||||||
|         pid = 0 |         pid = 0 | ||||||
|  |  | ||||||
|     if pid > 0: |     if pid > 0: | ||||||
| @@ -265,7 +265,7 @@ def certidude_signer_spawn(kill, no_interaction): | |||||||
|                 sleep(1) |                 sleep(1) | ||||||
|                 os.kill(pid, signal.SIGKILL) |                 os.kill(pid, signal.SIGKILL) | ||||||
|                 sleep(1) |                 sleep(1) | ||||||
|             except ProcessLookupError: |             except EnvironmentError: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|     child_pid = os.fork() |     child_pid = os.fork() | ||||||
| @@ -280,15 +280,7 @@ def certidude_signer_spawn(kill, no_interaction): | |||||||
|     logging.basicConfig( |     logging.basicConfig( | ||||||
|         filename="/var/log/signer.log", |         filename="/var/log/signer.log", | ||||||
|         level=logging.INFO) |         level=logging.INFO) | ||||||
|     server = SignServer( |     server = SignServer() | ||||||
|         config.SIGNER_SOCKET_PATH, |  | ||||||
|         config.AUTHORITY_PRIVATE_KEY_PATH, |  | ||||||
|         config.AUTHORITY_CERTIFICATE_PATH, |  | ||||||
|         config.CERTIFICATE_LIFETIME, |  | ||||||
|         config.CERTIFICATE_BASIC_CONSTRAINTS, |  | ||||||
|         config.CERTIFICATE_KEY_USAGE_FLAGS, |  | ||||||
|         config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS, |  | ||||||
|         config.REVOCATION_LIST_LIFETIME) |  | ||||||
|     asyncore.loop() |     asyncore.loop() | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -363,8 +355,8 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | |||||||
|         common_name, |         common_name, | ||||||
|         org_unit, |         org_unit, | ||||||
|         email_address, |         email_address, | ||||||
|         key_usage="nonRepudiation,digitalSignature,keyEncipherment", |         key_usage="digitalSignature,keyEncipherment", | ||||||
|         extended_key_usage="serverAuth,ikeIntermediate", |         extended_key_usage="serverAuth", | ||||||
|         wait=True) |         wait=True) | ||||||
|  |  | ||||||
|     if not os.path.exists(dhparam_path): |     if not os.path.exists(dhparam_path): | ||||||
| @@ -375,7 +367,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | |||||||
|         return retval |         return retval | ||||||
|  |  | ||||||
|     # TODO: Add dhparam |     # TODO: Add dhparam | ||||||
|     config.write(env.get_template("openvpn-site-to-client.ovpn").render(locals())) |     config.write(env.get_template("openvpn-site-to-client.ovpn").render(vars())) | ||||||
|  |  | ||||||
|     click.echo("Generated %s" % config.name) |     click.echo("Generated %s" % config.name) | ||||||
|     click.echo() |     click.echo() | ||||||
| @@ -385,6 +377,74 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | |||||||
|     click.echo() |     click.echo() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @click.command("nginx", help="Set up nginx as HTTPS server") | ||||||
|  | @click.argument("url") | ||||||
|  | @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) | ||||||
|  | @click.option("--org-unit", "-ou", help="Organizational unit") | ||||||
|  | @click.option("--tls-config", | ||||||
|  |     default="/etc/nginx/conf.d/tls.conf", | ||||||
|  |     type=click.File(mode="w", atomic=True, lazy=True), | ||||||
|  |     help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default") | ||||||
|  | @click.option("--site-config", "-o", | ||||||
|  |     default="/etc/nginx/sites-available/%s.conf" % HOSTNAME, | ||||||
|  |     type=click.File(mode="w", atomic=True, lazy=True), | ||||||
|  |     help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % HOSTNAME) | ||||||
|  | @click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default") | ||||||
|  | @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) | ||||||
|  | @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) | ||||||
|  | @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) | ||||||
|  | @click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") | ||||||
|  | @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default") | ||||||
|  | @click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off'])) | ||||||
|  | @expand_paths() | ||||||
|  | def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client): | ||||||
|  |     # TODO: Intelligent way of getting last IP address in the subnet | ||||||
|  |     from certidude.helpers import certidude_request_certificate | ||||||
|  |  | ||||||
|  |     if not os.path.exists(certificate_path): | ||||||
|  |         click.echo("As HTTPS server certificate needs specific key usage extensions please") | ||||||
|  |         click.echo("use following command to sign on Certidude server instead of web interface:") | ||||||
|  |         click.echo() | ||||||
|  |         click.echo("  certidude sign %s" % common_name) | ||||||
|  |         click.echo() | ||||||
|  |     retval = certidude_request_certificate(url, key_path, request_path, | ||||||
|  |         certificate_path, authority_path, common_name, org_unit, | ||||||
|  |         key_usage="digitalSignature,keyEncipherment", | ||||||
|  |         extended_key_usage="serverAuth", | ||||||
|  |         dns = constants.FQDN, wait=True, bundle=True) | ||||||
|  |  | ||||||
|  |     if not os.path.exists(dhparam_path): | ||||||
|  |         cmd = "openssl", "dhparam", "-out", dhparam_path, "2048" | ||||||
|  |         subprocess.check_call(cmd) | ||||||
|  |  | ||||||
|  |     if retval: | ||||||
|  |         return retval | ||||||
|  |  | ||||||
|  |     context = globals() # Grab constants.BLAH | ||||||
|  |     context.update(locals()) | ||||||
|  |  | ||||||
|  |     if os.path.exists(site_config.name): | ||||||
|  |         click.echo("Configuration file %s already exists, not overwriting" % site_config.name) | ||||||
|  |     else: | ||||||
|  |         site_config.write(env.get_template("nginx-https-site.conf").render(context)) | ||||||
|  |         click.echo("Generated %s" % site_config.name) | ||||||
|  |  | ||||||
|  |     if os.path.exists(tls_config.name): | ||||||
|  |         click.echo("Configuration file %s already exists, not overwriting" % tls_config.name) | ||||||
|  |     else: | ||||||
|  |         tls_config.write(env.get_template("nginx-tls.conf").render(context)) | ||||||
|  |         click.echo("Generated %s" % tls_config.name) | ||||||
|  |  | ||||||
|  |     click.echo() | ||||||
|  |     click.echo("Inspect configuration files, enable it and start nginx service:") | ||||||
|  |     click.echo() | ||||||
|  |     click.echo("  ln -s %s /etc/nginx/sites-enabled/%s" % ( | ||||||
|  |         os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"), | ||||||
|  |         os.path.basename(site_config.name))) | ||||||
|  |     click.secho("  service nginx restart", bold=True) | ||||||
|  |     click.echo() | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("client", help="Set up OpenVPN client") | @click.command("client", help="Set up OpenVPN client") | ||||||
| @click.argument("url") | @click.argument("url") | ||||||
| @click.argument("remote") | @click.argument("remote") | ||||||
| @@ -419,7 +479,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ | |||||||
|         return retval |         return retval | ||||||
|  |  | ||||||
|     # TODO: Add dhparam |     # TODO: Add dhparam | ||||||
|     config.write(env.get_template("openvpn-client-to-site.ovpn").render(locals())) |     config.write(env.get_template("openvpn-client-to-site.ovpn").render(vars())) | ||||||
|  |  | ||||||
|     click.echo("Generated %s" % config.name) |     click.echo("Generated %s" % config.name) | ||||||
|     click.echo() |     click.echo() | ||||||
| @@ -435,8 +495,8 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ | |||||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | @click.option("--org-unit", "-ou", help="Organizational unit") | ||||||
| @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") | @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") | ||||||
| @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL) | @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL) | ||||||
| @click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") | @click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") | ||||||
| @click.option("--local", "-l", default=None, type=ip_address, help="IP address associated with the certificate, none by default") | @click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default") | ||||||
| @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") | @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") | ||||||
| @click.option("--config", "-o", | @click.option("--config", "-o", | ||||||
|     default="/etc/ipsec.conf", |     default="/etc/ipsec.conf", | ||||||
| @@ -473,7 +533,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email | |||||||
|         common_name, |         common_name, | ||||||
|         org_unit, |         org_unit, | ||||||
|         email_address, |         email_address, | ||||||
|         key_usage="nonRepudiation,digitalSignature,keyEncipherment", |         key_usage="digitalSignature,keyEncipherment", | ||||||
|         extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", |         extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", | ||||||
|         ip_address=local, |         ip_address=local, | ||||||
|         dns=fqdn, |         dns=fqdn, | ||||||
| @@ -482,7 +542,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email | |||||||
|     if retval: |     if retval: | ||||||
|         return retval |         return retval | ||||||
|  |  | ||||||
|     config.write(env.get_template("strongswan-site-to-client.conf").render(locals())) |     config.write(env.get_template("strongswan-site-to-client.conf").render(vars())) | ||||||
|     secrets.write(": RSA %s\n" % key_path) |     secrets.write(": RSA %s\n" % key_path) | ||||||
|  |  | ||||||
|     click.echo("Generated %s and %s" % (config.name, secrets.name)) |     click.echo("Generated %s and %s" % (config.name, secrets.name)) | ||||||
| @@ -539,7 +599,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo | |||||||
|         return retval |         return retval | ||||||
|  |  | ||||||
|     # TODO: Add dhparam |     # TODO: Add dhparam | ||||||
|     config.write(env.get_template("strongswan-client-to-site.conf").render(locals())) |     config.write(env.get_template("strongswan-client-to-site.conf").render(vars())) | ||||||
|     secrets.write(": RSA %s\n" % key_path) |     secrets.write(": RSA %s\n" % key_path) | ||||||
|  |  | ||||||
|     click.echo("Generated %s and %s" % (config.name, secrets.name)) |     click.echo("Generated %s and %s" % (config.name, secrets.name)) | ||||||
| @@ -584,7 +644,7 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o | |||||||
|     csum = csummer.hexdigest() |     csum = csummer.hexdigest() | ||||||
|     uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] |     uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] | ||||||
|  |  | ||||||
|     config = configparser.ConfigParser() |     config = ConfigParser() | ||||||
|     config.add_section("connection") |     config.add_section("connection") | ||||||
|     config.add_section("vpn") |     config.add_section("vpn") | ||||||
|     config.add_section("ipv4") |     config.add_section("ipv4") | ||||||
| @@ -620,11 +680,12 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o | |||||||
|     subprocess.call(("nmcli", "c", "up", "uuid", uuid)) |     subprocess.call(("nmcli", "c", "up", "uuid", uuid)) | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("production", help="Set up nginx and uwsgi") | @click.command("production", help="Set up nginx, uwsgi and cron") | ||||||
| @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") | @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") | ||||||
| @click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME) | @click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME) | ||||||
| @click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files") | @click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files") | ||||||
| @click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab") | @click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab") | ||||||
|  | @click.option("--push-server", default=None, help="Push server URL") | ||||||
| @click.option("--nginx-config", "-n", | @click.option("--nginx-config", "-n", | ||||||
|     default="/etc/nginx/nginx.conf", |     default="/etc/nginx/nginx.conf", | ||||||
|     type=click.File(mode="w", atomic=True, lazy=True), |     type=click.File(mode="w", atomic=True, lazy=True), | ||||||
| @@ -642,19 +703,36 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | |||||||
|         subprocess.check_call(cmd) |         subprocess.check_call(cmd) | ||||||
|  |  | ||||||
|     if subprocess.call("net ads testjoin", shell=True): |     if subprocess.call("net ads testjoin", shell=True): | ||||||
|         click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", stderr=True) |         click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", err=True) | ||||||
|         exit(255) |         exit(255) | ||||||
|  |  | ||||||
|     if not os.path.exists(kerberos_keytab): |     if not os.path.exists(kerberos_keytab): | ||||||
|         subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P") |         subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P") | ||||||
|         click.echo("Created Kerberos keytab in '%s'" % kerberos_keytab) |         click.echo("Created service principal in Kerberos keytab '%s'" % kerberos_keytab) | ||||||
|  |  | ||||||
|  |     if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): | ||||||
|  |         # Fetch Kerberos ticket for system account | ||||||
|  |         cp = ConfigParser() | ||||||
|  |         cp.read("/etc/samba/smb.conf") | ||||||
|  |         domain = cp.get("global", "realm").lower() | ||||||
|  |         base = ",".join(["dc=" + j for j in domain.split(".")]) | ||||||
|  |         with open("/etc/cron.hourly/certidude", "w") as fh: | ||||||
|  |             fh.write("#!/bin/bash\n") | ||||||
|  |             fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name")) | ||||||
|  |             fh.write("chown certidude /run/certidude/krb5cc-new\n") | ||||||
|  |             fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n") | ||||||
|  |         os.chmod("/etc/cron.hourly/certidude", 0o755) | ||||||
|  |         click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal") | ||||||
|  |     else: | ||||||
|  |         click.echo("Warning: cronjob for Kerberos ticket renewal not created, LDAP with GSSAPI will not be available!") | ||||||
|  |  | ||||||
|  |  | ||||||
|     if not static_path.endswith("/"): |     if not static_path.endswith("/"): | ||||||
|         static_path += "/" |         static_path += "/" | ||||||
|  |  | ||||||
|     nginx_config.write(env.get_template("nginx.conf").render(locals())) |     nginx_config.write(env.get_template("nginx.conf").render(vars())) | ||||||
|     click.echo("Generated: %s" % nginx_config.name) |     click.echo("Generated: %s" % nginx_config.name) | ||||||
|     uwsgi_config.write(env.get_template("uwsgi.ini").render(locals())) |     uwsgi_config.write(env.get_template("uwsgi.ini").render(vars())) | ||||||
|     click.echo("Generated: %s" % uwsgi_config.name) |     click.echo("Generated: %s" % uwsgi_config.name) | ||||||
|  |  | ||||||
|     if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): |     if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): | ||||||
| @@ -663,7 +741,7 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | |||||||
|     click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name) |     click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name) | ||||||
|  |  | ||||||
|     if not push_server: |     if not push_server: | ||||||
|         click.echo("Remember to install nginx with wandenberg/nginx-push-stream-module!") |         click.echo("Remember to install nchan instead of regular nginx!") | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("authority", help="Set up Certificate Authority in a directory") | @click.command("authority", help="Set up Certificate Authority in a directory") | ||||||
| @@ -735,6 +813,9 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|     ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) |     ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) | ||||||
|     ca.set_issuer(ca.get_subject()) |     ca.set_issuer(ca.get_subject()) | ||||||
|     ca.set_pubkey(key) |     ca.set_pubkey(key) | ||||||
|  |  | ||||||
|  |     # add_extensions shall be called only once and | ||||||
|  |     # there has to be only one subjectAltName! | ||||||
|     ca.add_extensions([ |     ca.add_extensions([ | ||||||
|         crypto.X509Extension( |         crypto.X509Extension( | ||||||
|             b"basicConstraints", |             b"basicConstraints", | ||||||
| @@ -746,7 +827,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|             b"keyCertSign, cRLSign"), |             b"keyCertSign, cRLSign"), | ||||||
|         crypto.X509Extension( |         crypto.X509Extension( | ||||||
|             b"extendedKeyUsage", |             b"extendedKeyUsage", | ||||||
|             True, |             False, | ||||||
|             b"serverAuth,1.3.6.1.5.5.8.2.2"), |             b"serverAuth,1.3.6.1.5.5.8.2.2"), | ||||||
|         crypto.X509Extension( |         crypto.X509Extension( | ||||||
|             b"subjectKeyIdentifier", |             b"subjectKeyIdentifier", | ||||||
| @@ -756,21 +837,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|         crypto.X509Extension( |         crypto.X509Extension( | ||||||
|             b"crlDistributionPoints", |             b"crlDistributionPoints", | ||||||
|             False, |             False, | ||||||
|             crl_distribution_points.encode("ascii")) |             crl_distribution_points.encode("ascii")), | ||||||
|     ]) |  | ||||||
|  |  | ||||||
|     subject_alt_name = "email:%s" % email_address |  | ||||||
|     ca.add_extensions([ |  | ||||||
|         crypto.X509Extension( |         crypto.X509Extension( | ||||||
|             b"subjectAltName", |             b"subjectAltName", | ||||||
|             False, |             False, | ||||||
|             subject_alt_name.encode("ascii")) |             "DNS: %s, email: %s" % (common_name.encode("ascii"), email_address.encode("ascii"))) | ||||||
|     ]) |  | ||||||
|     ca.add_extensions([ |  | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"subjectAltName", |  | ||||||
|             True, |  | ||||||
|             ("DNS:%s" % common_name).encode("ascii")) |  | ||||||
|     ]) |     ]) | ||||||
|  |  | ||||||
|     if ocsp_responder_url: |     if ocsp_responder_url: | ||||||
| @@ -819,7 +890,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|     # Set permission bits to 640 |     # Set permission bits to 640 | ||||||
|     os.umask(0o137) |     os.umask(0o137) | ||||||
|     with open(certidude_conf, "w") as fh: |     with open(certidude_conf, "w") as fh: | ||||||
|         fh.write(env.get_template("certidude.conf").render(locals())) |         fh.write(env.get_template("certidude.conf").render(vars())) | ||||||
|     with open(ca_crt, "wb") as fh: |     with open(ca_crt, "wb") as fh: | ||||||
|         fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) |         fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) | ||||||
|  |  | ||||||
| @@ -988,12 +1059,23 @@ def certidude_sign(common_name, overwrite, lifetime): | |||||||
|         click.echo("Added extension %s: %s" % (key, value)) |         click.echo("Added extension %s: %s" % (key, value)) | ||||||
|     click.echo() |     click.echo() | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("serve", help="Run built-in HTTP server") | @click.command("serve", help="Run built-in HTTP server") | ||||||
| @click.option("-u", "--user", default="certidude", help="Run as user") | @click.option("-u", "--user", default="certidude", help="Run as user") | ||||||
| @click.option("-p", "--port", default=80, help="Listen port") | @click.option("-p", "--port", default=80, help="Listen port") | ||||||
| @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") | @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") | ||||||
| @click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA") | @click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA") | ||||||
| def certidude_serve(user, port, listen, enable_signature): | def certidude_serve(user, port, listen, enable_signature): | ||||||
|  |     from certidude import config | ||||||
|  |  | ||||||
|  |     click.echo("Users subnets: %s" % | ||||||
|  |         ", ".join([str(j) for j in config.USER_SUBNETS])) | ||||||
|  |     click.echo("Administrative subnets: %s" % | ||||||
|  |         ", ".join([str(j) for j in config.ADMIN_SUBNETS])) | ||||||
|  |     click.echo("Auto-sign enabled for following subnets: %s" % | ||||||
|  |         ", ".join([str(j) for j in config.AUTOSIGN_SUBNETS])) | ||||||
|  |     click.echo("Request submissions allowed from following subnets: %s" % | ||||||
|  |         ", ".join([str(j) for j in config.REQUEST_SUBNETS])) | ||||||
|  |  | ||||||
|     logging.basicConfig( |     logging.basicConfig( | ||||||
|         filename='/var/log/certidude.log', |         filename='/var/log/certidude.log', | ||||||
| @@ -1004,13 +1086,15 @@ def certidude_serve(user, port, listen, enable_signature): | |||||||
|     from wsgiref.simple_server import make_server, WSGIServer |     from wsgiref.simple_server import make_server, WSGIServer | ||||||
|     from socketserver import ThreadingMixIn |     from socketserver import ThreadingMixIn | ||||||
|     from certidude.api import certidude_app, StaticResource |     from certidude.api import certidude_app, StaticResource | ||||||
|     from certidude import config |  | ||||||
|  |  | ||||||
|     class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): |     class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     click.echo("Listening on %s:%d" % (listen, port)) |     click.echo("Listening on %s:%d" % (listen, port)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # TODO: Bind before dropping privileges, | ||||||
|  |     #       but create app (sqlite log files!) after dropping privileges | ||||||
|     app = certidude_app() |     app = certidude_app() | ||||||
|  |  | ||||||
|     app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static"))) |     app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static"))) | ||||||
| @@ -1023,25 +1107,25 @@ def certidude_serve(user, port, listen, enable_signature): | |||||||
|         from jinja2.debug import make_traceback as _make_traceback |         from jinja2.debug import make_traceback as _make_traceback | ||||||
|         "".encode("charmap") |         "".encode("charmap") | ||||||
|  |  | ||||||
|         if config.AUTHENTICATION_BACKEND == "pam": |         restricted_groups = [] | ||||||
|  |  | ||||||
|  |         if config.AUTHENTICATION_BACKENDS == {"pam"}: | ||||||
|             # PAM needs access to /etc/shadow |             # PAM needs access to /etc/shadow | ||||||
|             import grp |             import grp | ||||||
|             name, passwd, gid, mem = grp.getgrnam("shadow") |             name, passwd, gid, mem = grp.getgrnam("shadow") | ||||||
|             click.echo("Adding current user to shadow group due to PAM authentication backend") |             click.echo("Adding current user to shadow group due to PAM authentication backend") | ||||||
|             os.setgroups([gid]) |             restricted_groups.append(gid) | ||||||
|         else: |  | ||||||
|             os.setgroups([]) |  | ||||||
|  |  | ||||||
|         _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) |         _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) | ||||||
|         if uid == 0: |         restricted_groups.append(gid) | ||||||
|             click.echo("Please specify unprivileged user") |  | ||||||
|             exit(254) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         os.setgroups(restricted_groups) | ||||||
|         os.setgid(gid) |         os.setgid(gid) | ||||||
|         os.setuid(uid) |         os.setuid(uid) | ||||||
|  |  | ||||||
|         click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % |         click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % | ||||||
|             (user, uid, gid, ", ".join([str(j) for j in os.getgroups()]))) |             (user, os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) | ||||||
|  |  | ||||||
|         os.umask(0o007) |         os.umask(0o007) | ||||||
|     elif os.getuid() == 0: |     elif os.getuid() == 0: | ||||||
| @@ -1076,6 +1160,7 @@ certidude_setup.add_command(certidude_setup_openvpn) | |||||||
| certidude_setup.add_command(certidude_setup_strongswan) | certidude_setup.add_command(certidude_setup_strongswan) | ||||||
| certidude_setup.add_command(certidude_setup_client) | certidude_setup.add_command(certidude_setup_client) | ||||||
| certidude_setup.add_command(certidude_setup_production) | certidude_setup.add_command(certidude_setup_production) | ||||||
|  | certidude_setup.add_command(certidude_setup_nginx) | ||||||
| certidude_request.add_command(certidude_request_spawn) | certidude_request.add_command(certidude_request_spawn) | ||||||
| certidude_signer.add_command(certidude_signer_spawn) | certidude_signer.add_command(certidude_signer_spawn) | ||||||
| entry_point.add_command(certidude_setup) | entry_point.add_command(certidude_setup) | ||||||
|   | |||||||
| @@ -1,6 +1,13 @@ | |||||||
|  |  | ||||||
| import os | import os | ||||||
| import click | import click | ||||||
|  | import ipaddress | ||||||
|  |  | ||||||
|  | def ip_network(j): | ||||||
|  |     return ipaddress.ip_network(unicode(j)) | ||||||
|  |  | ||||||
|  | def ip_address(j): | ||||||
|  |     return ipaddress.ip_address(unicode(j)) | ||||||
|  |  | ||||||
| def expand_paths(): | def expand_paths(): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -4,50 +4,52 @@ import codecs | |||||||
| import configparser | import configparser | ||||||
| import ipaddress | import ipaddress | ||||||
| import os | import os | ||||||
| import socket |  | ||||||
| import string | import string | ||||||
| from random import choice | from random import choice | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] |  | ||||||
|  |  | ||||||
| cp = configparser.ConfigParser() | cp = configparser.ConfigParser() | ||||||
| cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) | cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) | ||||||
|  |  | ||||||
| AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam | AUTHENTICATION_BACKENDS = set([j for j in | ||||||
| AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam |     cp.get("authentication", "backends").split(" ") if j])   # kerberos, pam, ldap | ||||||
|  | AUTHORIZATION_BACKEND = cp.get("authorization", "backend")  # whitelist, ldap, posix | ||||||
|  | ACCOUNTS_BACKEND = cp.get("accounts", "backend")             # posix, ldap | ||||||
|  |  | ||||||
| ADMIN_USERS = set([j for j in  cp.get("authorization", "admin_users").split(" ") if j]) | USER_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
| ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j]) |     cp.get("authorization", "user subnets").split(" ") if j]) | ||||||
| AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign_subnets").split(" ") if j]) | ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
| REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "request_subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) |     cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS) | ||||||
|  | AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|  |     cp.get("authorization", "autosign subnets").split(" ") if j]) | ||||||
|  | REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|  |     cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) | ||||||
|  |  | ||||||
| SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" | SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" | ||||||
| SIGNER_PID_PATH = "/run/certidude/signer.pid" | SIGNER_PID_PATH = "/run/certidude/signer.pid" | ||||||
|  |  | ||||||
| AUTHORITY_DIR = "/var/lib/certidude" | AUTHORITY_DIR = "/var/lib/certidude" | ||||||
| AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private_key_path") | AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | ||||||
| AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate_path") | AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") | ||||||
| REQUESTS_DIR = cp.get("authority", "requests_dir") | REQUESTS_DIR = cp.get("authority", "requests dir") | ||||||
| SIGNED_DIR = cp.get("authority", "signed_dir") | SIGNED_DIR = cp.get("authority", "signed dir") | ||||||
| REVOKED_DIR = cp.get("authority", "revoked_dir") | REVOKED_DIR = cp.get("authority", "revoked dir") | ||||||
|  | OUTBOX = cp.get("authority", "outbox") | ||||||
| #LOG_DATA = cp.get("logging", "database") |  | ||||||
|  |  | ||||||
| CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" | CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" | ||||||
| CERTIFICATE_KEY_USAGE_FLAGS = "nonRepudiation,digitalSignature,keyEncipherment" | CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment" | ||||||
| CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth" | CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth" | ||||||
| CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate_lifetime")) | CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate lifetime")) | ||||||
|  |  | ||||||
| REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation_list_lifetime")) | REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation list lifetime")) | ||||||
|  |  | ||||||
| PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)]) | PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)]) | ||||||
|  |  | ||||||
| PUSH_TOKEN = "ca" | PUSH_TOKEN = "ca" | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     PUSH_EVENT_SOURCE = cp.get("push", "event_source") |     PUSH_EVENT_SOURCE = cp.get("push", "event source") | ||||||
|     PUSH_LONG_POLL = cp.get("push", "long_poll") |     PUSH_LONG_POLL = cp.get("push", "long poll") | ||||||
|     PUSH_PUBLISH = cp.get("push", "publish") |     PUSH_PUBLISH = cp.get("push", "publish") | ||||||
| except configparser.NoOptionError: | except configparser.NoOptionError: | ||||||
|     PUSH_SERVER = cp.get("push", "server") or "http://localhost" |     PUSH_SERVER = cp.get("push", "server") or "http://localhost" | ||||||
| @@ -55,18 +57,41 @@ except configparser.NoOptionError: | |||||||
|     PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" |     PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" | ||||||
|     PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" |     PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" | ||||||
|  |  | ||||||
| o = urlparse(cp.get("authority", "database") if cp.has_option("authority", "database") else "") |  | ||||||
|  |  | ||||||
| if not o.scheme: | TAGGING_BACKEND = cp.get("tagging", "backend") | ||||||
|     DATABASE_POOL = None | LOGGING_BACKEND = cp.get("logging", "backend") | ||||||
| elif o.scheme == "mysql": | LEASES_BACKEND = cp.get("leases", "backend") | ||||||
|     import mysql.connector |  | ||||||
|     DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool( |  | ||||||
|         pool_size = 32, | if "whitelist" == AUTHORIZATION_BACKEND: | ||||||
|         user=o.username, |     USERS_WHITELIST = set([j for j in  cp.get("authorization", "users whitelist").split(" ") if j]) | ||||||
|         password=o.password, |     ADMINS_WHITELIST = set([j for j in  cp.get("authorization", "admins whitelist").split(" ") if j]) | ||||||
|         host=o.hostname, | elif "posix" == AUTHORIZATION_BACKEND: | ||||||
|         database=o.path[1:]) |     USERS_GROUP = cp.get("authorization", "posix user group") | ||||||
|  |     ADMINS_GROUP = cp.get("authorization", "posix admin group") | ||||||
|  | elif "ldap" == AUTHORIZATION_BACKEND: | ||||||
|  |     USERS_GROUP = cp.get("authorization", "ldap user group") | ||||||
|  |     ADMINS_GROUP = cp.get("authorization", "ldap admin group") | ||||||
| else: | else: | ||||||
|     raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database is supported" % o.scheme) |     raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) | ||||||
|  |  | ||||||
|  | LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | ||||||
|  | LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter") | ||||||
|  | LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter") | ||||||
|  | LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter") | ||||||
|  |  | ||||||
|  | for line in open("/etc/ldap/ldap.conf"): | ||||||
|  |     line = line.strip().lower() | ||||||
|  |     if "#" in line: | ||||||
|  |         line, _ = line.split("#", 1) | ||||||
|  |     if not " " in line: | ||||||
|  |         continue | ||||||
|  |     key, value = line.split(" ", 1) | ||||||
|  |     if key == "uri": | ||||||
|  |         LDAP_SERVERS = set([j for j in value.split(" ") if j]) | ||||||
|  |         click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) | ||||||
|  |     elif key == "base": | ||||||
|  |         LDAP_BASE = value | ||||||
|  | else: | ||||||
|  |     click.echo("No LDAP servers specified in /etc/ldap/ldap.conf") | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								certidude/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 falcon | ||||||
| import ipaddress | import ipaddress | ||||||
| import json | import json | ||||||
|  | import logging | ||||||
| import re | import re | ||||||
| import types | import types | ||||||
| from datetime import date, time, datetime | from datetime import date, time, datetime | ||||||
| from OpenSSL import crypto | from OpenSSL import crypto | ||||||
| from certidude.wrappers import Request, Certificate | from certidude.wrappers import Request, Certificate | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
|  | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
|  | def csrf_protection(func): | ||||||
|  |     """ | ||||||
|  |     Protect resource from common CSRF attacks by checking user agent and referrer | ||||||
|  |     """ | ||||||
|  |     def wrapped(self, req, resp, *args, **kwargs): | ||||||
|  |         # Assume curl and python-requests are used intentionally | ||||||
|  |         if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"): | ||||||
|  |             return func(self, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         # For everything else assert referrer | ||||||
|  |         referrer = req.headers.get("REFERER") | ||||||
|  |         if referrer: | ||||||
|  |             scheme, netloc, path, params, query, fragment = urlparse(referrer) | ||||||
|  |             if netloc == req.host: | ||||||
|  |                 return func(self, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         # Kaboom! | ||||||
|  |         logger.warning("Prevented clickbait from '%s' with user agent '%s'", | ||||||
|  |             referrer or "-", req.user_agent) | ||||||
|  |         raise falcon.HTTPUnauthorized("Forbidden", | ||||||
|  |             "No suitable UA or referrer provided, cross-site scripting disabled") | ||||||
|  |     return wrapped | ||||||
|  |  | ||||||
|  |  | ||||||
| def event_source(func): | def event_source(func): | ||||||
|     def wrapped(self, req, resp, *args, **kwargs): |     def wrapped(self, req, resp, *args, **kwargs): | ||||||
| @@ -15,7 +41,6 @@ def event_source(func): | |||||||
|             resp.status = falcon.HTTP_SEE_OTHER |             resp.status = falcon.HTTP_SEE_OTHER | ||||||
|             resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid |             resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid | ||||||
|             resp.body = "Redirecting to:" + resp.location |             resp.body = "Redirecting to:" + resp.location | ||||||
|             print("Delegating EventSource handling to:", resp.location) |  | ||||||
|         return func(self, req, resp, *args, **kwargs) |         return func(self, req, resp, *args, **kwargs) | ||||||
|     return wrapped |     return wrapped | ||||||
|  |  | ||||||
| @@ -24,9 +49,10 @@ class MyEncoder(json.JSONEncoder): | |||||||
|         "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ |         "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ | ||||||
|         "key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage" |         "key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage" | ||||||
|  |  | ||||||
|     CERTIFICATE_ATTRIBUTES = "revokable", "identity", "changed", "common_name", \ |     CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \ | ||||||
|         "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ |         "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ | ||||||
|         "key_type", "key_length", "sha256sum", "serial_number", "key_usage" |         "key_type", "key_length", "sha256sum", "serial_number", "key_usage", \ | ||||||
|  |         "signed", "expires" | ||||||
|  |  | ||||||
|     def default(self, obj): |     def default(self, obj): | ||||||
|         if isinstance(obj, crypto.X509Name): |         if isinstance(obj, crypto.X509Name): | ||||||
| @@ -60,18 +86,25 @@ def serialize(func): | |||||||
|     Falcon response serialization |     Falcon response serialization | ||||||
|     """ |     """ | ||||||
|     def wrapped(instance, req, resp, **kwargs): |     def wrapped(instance, req, resp, **kwargs): | ||||||
|         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" |         resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate") | ||||||
|         resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); |         resp.set_header("Pragma", "no-cache") | ||||||
|         resp.set_header("Pragma", "no-cache"); |         resp.set_header("Expires", "0") | ||||||
|         resp.set_header("Expires", "0"); |  | ||||||
|         r = func(instance, req, resp, **kwargs) |         r = func(instance, req, resp, **kwargs) | ||||||
|         if resp.body is None: |         if resp.body is None: | ||||||
|             if req.get_header("Accept").split(",")[0] == "application/json": |             if req.accept.startswith("application/json"): | ||||||
|                 resp.set_header("Content-Type", "application/json") |                 resp.set_header("Content-Type", "application/json") | ||||||
|                 resp.set_header("Content-Disposition", "inline") |                 resp.set_header("Content-Disposition", "inline") | ||||||
|                 resp.body = json.dumps(r, cls=MyEncoder) |                 resp.body = json.dumps(r, cls=MyEncoder) | ||||||
|  |  | ||||||
|  |             elif hasattr(r, "content_type") and req.client_accepts(r.content_type): | ||||||
|  |                 resp.set_header("Content-Type", r.content_type) | ||||||
|  |                 resp.set_header("Content-Disposition", | ||||||
|  |                     ("attachment; filename=%s" % r.suggested_filename).encode("ascii")) | ||||||
|  |                 resp.body = r.dump() | ||||||
|             else: |             else: | ||||||
|                 resp.body = repr(r) |                 logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept)) | ||||||
|  |                 raise falcon.HTTPUnsupportedMediaType( | ||||||
|  |                     "Client did not accept application/json or %s" % r.content_type) | ||||||
|         return r |         return r | ||||||
|     return wrapped |     return wrapped | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								certidude/firewall.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 certidude.wrappers import Certificate, Request | ||||||
| from OpenSSL import crypto | from OpenSSL import crypto | ||||||
|  |  | ||||||
| def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None): | def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): | ||||||
|     """ |     """ | ||||||
|     Exchange CSR for certificate using Certidude HTTP API server |     Exchange CSR for certificate using Certidude HTTP API server | ||||||
|     """ |     """ | ||||||
| @@ -41,7 +41,8 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|         click.echo("Attempting to fetch CA certificate from %s" % authority_url) |         click.echo("Attempting to fetch CA certificate from %s" % authority_url) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             r = requests.get(authority_url) |             r = requests.get(authority_url, | ||||||
|  |                     headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) | ||||||
|             cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) |             cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) | ||||||
|         except crypto.Error: |         except crypto.Error: | ||||||
|             raise ValueError("Failed to parse PEM: %s" % r.text) |             raise ValueError("Failed to parse PEM: %s" % r.text) | ||||||
| @@ -53,7 +54,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|     try: |     try: | ||||||
|         request = Request(open(request_path)) |         request = Request(open(request_path)) | ||||||
|         click.echo("Found signing request: %s" % request_path) |         click.echo("Found signing request: %s" % request_path) | ||||||
|     except FileNotFoundError: |     except EnvironmentError: | ||||||
|  |  | ||||||
|         # Construct private key |         # Construct private key | ||||||
|         click.echo("Generating 4096-bit RSA key...") |         click.echo("Generating 4096-bit RSA key...") | ||||||
| @@ -69,10 +70,11 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|         csr = crypto.X509Req() |         csr = crypto.X509Req() | ||||||
|         csr.set_version(2) # Corresponds to X.509v3 |         csr.set_version(2) # Corresponds to X.509v3 | ||||||
|         csr.set_pubkey(key) |         csr.set_pubkey(key) | ||||||
|  |         csr.get_subject().CN = common_name | ||||||
|  |  | ||||||
|         request = Request(csr) |         request = Request(csr) | ||||||
|  |  | ||||||
|         # Set subject attributes |         # Set subject attributes | ||||||
|         request.common_name = common_name |  | ||||||
|         if given_name: |         if given_name: | ||||||
|             request.given_name = given_name |             request.given_name = given_name | ||||||
|         if surname: |         if surname: | ||||||
| @@ -83,20 +85,20 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|         # Collect subject alternative names |         # Collect subject alternative names | ||||||
|         subject_alt_name = set() |         subject_alt_name = set() | ||||||
|         if email_address: |         if email_address: | ||||||
|             subject_alt_name.add("email:" + email_address) |             subject_alt_name.add("email:%s" % email_address) | ||||||
|         if ip_address: |         if ip_address: | ||||||
|             subject_alt_name.add("IP:" + ip_address) |             subject_alt_name.add("IP:%s" % ip_address) | ||||||
|         if dns: |         if dns: | ||||||
|             subject_alt_name.add("DNS:" + dns) |             subject_alt_name.add("DNS:%s" % dns) | ||||||
|  |  | ||||||
|         # Set extensions |         # Set extensions | ||||||
|         extensions = [] |         extensions = [] | ||||||
|         if key_usage: |         if key_usage: | ||||||
|             extensions.append(("keyUsage", key_usage, True)) |             extensions.append(("keyUsage", key_usage, True)) | ||||||
|         if extended_key_usage: |         if extended_key_usage: | ||||||
|             extensions.append(("extendedKeyUsage", extended_key_usage, True)) |             extensions.append(("extendedKeyUsage", extended_key_usage, False)) | ||||||
|         if subject_alt_name: |         if subject_alt_name: | ||||||
|             extensions.append(("subjectAltName", ", ".join(subject_alt_name), True)) |             extensions.append(("subjectAltName", ", ".join(subject_alt_name), False)) | ||||||
|         request.set_extensions(extensions) |         request.set_extensions(extensions) | ||||||
|  |  | ||||||
|         # Dump CSR |         # Dump CSR | ||||||
| @@ -113,7 +115,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|     click.echo("Submitting to %s, waiting for response..." % request_url) |     click.echo("Submitting to %s, waiting for response..." % request_url) | ||||||
|     submission = requests.post(request_url, |     submission = requests.post(request_url, | ||||||
|         data=open(request_path), |         data=open(request_path), | ||||||
|         headers={"User-Agent": "Certidude", "Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert"}) |         headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"}) | ||||||
|  |  | ||||||
|     if submission.status_code == requests.codes.ok: |     if submission.status_code == requests.codes.ok: | ||||||
|         pass |         pass | ||||||
| @@ -131,12 +133,18 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | |||||||
|     try: |     try: | ||||||
|         cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) |         cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) | ||||||
|     except crypto.Error: |     except crypto.Error: | ||||||
|         raise ValueError("Failed to parse PEM: %s" % buf) |         raise ValueError("Failed to parse PEM: %s" % submission.text) | ||||||
|  |  | ||||||
|     os.umask(0o022) |     os.umask(0o022) | ||||||
|     with open(certificate_path + ".part", "w") as fh: |     with open(certificate_path + ".part", "w") as fh: | ||||||
|  |         # Dump certificate | ||||||
|         fh.write(submission.text) |         fh.write(submission.text) | ||||||
|  |  | ||||||
|  |         # Bundle CA certificate, necessary for nginx | ||||||
|  |         if bundle: | ||||||
|  |             with open(authority_path) as ch: | ||||||
|  |                 fh.write(ch.read()) | ||||||
|  |  | ||||||
|     click.echo("Writing certificate to: %s" % certificate_path) |     click.echo("Writing certificate to: %s" % certificate_path) | ||||||
|     os.rename(certificate_path + ".part", certificate_path) |     os.rename(certificate_path + ".part", certificate_path) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,25 @@ | |||||||
|  |  | ||||||
| import os | import os | ||||||
| import smtplib | import smtplib | ||||||
| from time import sleep | from markdown import markdown | ||||||
| from jinja2 import Environment, PackageLoader | from jinja2 import Environment, PackageLoader | ||||||
| from email.mime.multipart import MIMEMultipart | from email.mime.multipart import MIMEMultipart | ||||||
| from email.mime.text import MIMEText | from email.mime.text import MIMEText | ||||||
|  | from email.mime.base import MIMEBase | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| class Mailer(object): | env = Environment(loader=PackageLoader("certidude", "templates/mail")) | ||||||
|     def __init__(self, url): |  | ||||||
|         scheme, netloc, path, params, query, fragment = urlparse(url) | 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() |     scheme = scheme.lower() | ||||||
|  |  | ||||||
|     if path: |     if path: | ||||||
| @@ -22,15 +32,15 @@ class Mailer(object): | |||||||
|         raise ValueError("Fragment for URL not supported") |         raise ValueError("Fragment for URL not supported") | ||||||
|  |  | ||||||
|  |  | ||||||
|         self.username = None |     username = None | ||||||
|         self.password = "" |     password = "" | ||||||
|  |  | ||||||
|     if scheme == "smtp": |     if scheme == "smtp": | ||||||
|             self.secure = False |         secure = False | ||||||
|             self.port = 25 |         port = 25 | ||||||
|     elif scheme == "smtps": |     elif scheme == "smtps": | ||||||
|             self.secure = True |         secure = True | ||||||
|             self.port = 465 |         port = 465 | ||||||
|     else: |     else: | ||||||
|         raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme) |         raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme) | ||||||
|  |  | ||||||
| @@ -38,51 +48,24 @@ class Mailer(object): | |||||||
|         credentials, netloc = netloc.split("@") |         credentials, netloc = netloc.split("@") | ||||||
|  |  | ||||||
|         if ":" in credentials: |         if ":" in credentials: | ||||||
|                 self.username, self.password = credentials.split(":") |             username, password = credentials.split(":") | ||||||
|         else: |         else: | ||||||
|                 self.username = credentials |             username = credentials | ||||||
|  |  | ||||||
|     if ":" in netloc: |     if ":" in netloc: | ||||||
|             self.server, port_str = netloc.split(":") |         server, port_str = netloc.split(":") | ||||||
|             self.port = int(port_str) |         port = int(port_str) | ||||||
|     else: |     else: | ||||||
|             self.server = netloc |         server = netloc | ||||||
|  |  | ||||||
|         self.env = Environment(loader=PackageLoader("certidude", "email_templates")) |  | ||||||
|         self.conn = None |  | ||||||
|  |  | ||||||
|     def reconnect(self): |  | ||||||
|         # Gmail employs some sort of IPS |  | ||||||
|         # https://accounts.google.com/DisplayUnlockCaptcha |  | ||||||
|         print("Connecting to:", self.server, self.port) |  | ||||||
|         self.conn = smtplib.SMTP(self.server, self.port) |  | ||||||
|         if self.secure: |  | ||||||
|             self.conn.starttls() |  | ||||||
|         if self.username and self.password: |  | ||||||
|             self.conn.login(self.username, self.password) |  | ||||||
|  |  | ||||||
|     def enqueue(self, sender, recipients, subject, template, **context): |  | ||||||
|         self.send(sender, recipients, subject, template, **context) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def send(self, sender, recipients, subject, template, **context): |     subject, text = env.get_template(template).render(context).split("\n\n", 1) | ||||||
|  |     html = markdown(text) | ||||||
|         recipients = [j for j in recipients if j] |  | ||||||
|  |  | ||||||
|         if not recipients: |  | ||||||
|             print("No recipients to send e-mail to!") |  | ||||||
|             return |  | ||||||
|         print("Sending e-mail to:", recipients, "body follows:") |  | ||||||
|  |  | ||||||
|     msg = MIMEMultipart("alternative") |     msg = MIMEMultipart("alternative") | ||||||
|     msg["Subject"] = subject |     msg["Subject"] = subject | ||||||
|         msg["From"] = sender |     msg["From"] = authority.certificate.email_address | ||||||
|         msg["To"] = ", ".join(recipients) |     msg["To"] = recipients | ||||||
|  |  | ||||||
|         text = self.env.get_template(template + ".txt").render(context) |  | ||||||
|         html = self.env.get_template(template + ".html").render(context) |  | ||||||
|  |  | ||||||
|         print(text) |  | ||||||
|  |  | ||||||
|     part1 = MIMEText(text, "plain") |     part1 = MIMEText(text, "plain") | ||||||
|     part2 = MIMEText(html, "html") |     part2 = MIMEText(html, "html") | ||||||
| @@ -90,15 +73,18 @@ class Mailer(object): | |||||||
|     msg.attach(part1) |     msg.attach(part1) | ||||||
|     msg.attach(part2) |     msg.attach(part2) | ||||||
|  |  | ||||||
|         backoff = 1 |     for attachment in attachments: | ||||||
|         while True: |         part = MIMEBase(*attachment.content_type.split("/")) | ||||||
|             try: |         part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename) | ||||||
|                 if not self.conn: |         part.set_payload(attachment.dump()) | ||||||
|                     self.reconnect() |         msg.attach(part) | ||||||
|                 self.conn.sendmail(sender, recipients, msg.as_string()) |  | ||||||
|                 return |     # Gmail employs some sort of IPS | ||||||
|             except smtplib.SMTPServerDisconnected: |     # https://accounts.google.com/DisplayUnlockCaptcha | ||||||
|                 print("Connection to %s unexpectedly closed, probably TCP timeout, backing off for %d second" % (self.server, backoff)) |     conn = smtplib.SMTP(server, port) | ||||||
|                 self.reconnect() |     if secure: | ||||||
|                 backoff = backoff * 2 |         conn.starttls() | ||||||
|                 sleep(backoff) |     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 logging | ||||||
| import time | import time | ||||||
|  | from certidude.api.tag import RelationalMixin | ||||||
|   |   | ||||||
| class MySQLLogHandler(logging.Handler): | class LogHandler(logging.Handler, RelationalMixin): | ||||||
|  |     SQL_CREATE_TABLES = "log_tables.sql" | ||||||
|   |   | ||||||
|     SQL_CREATE_TABLE = """CREATE TABLE IF NOT EXISTS log( |     def __init__(self, uri): | ||||||
|         created datetime, facility varchar(30), level int, |  | ||||||
|         severity varchar(10), message text, module varchar(20), |  | ||||||
|         func varchar(20), lineno int, exception text, process int, |  | ||||||
|         thread text, thread_name text)""" |  | ||||||
|   |  | ||||||
|     SQL_INSERT_ENTRY = """insert into log( created, facility, level, severity, |  | ||||||
|         message, module, func, lineno, exception, process, thread, |  | ||||||
|         thread_name) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); |  | ||||||
|         """ |  | ||||||
|   |  | ||||||
|     def __init__(self, pool): |  | ||||||
|         logging.Handler.__init__(self) |         logging.Handler.__init__(self) | ||||||
|         self.pool = pool |         RelationalMixin.__init__(self, uri) | ||||||
|         conn = self.pool.get_connection() |  | ||||||
|         cur = conn.cursor() |  | ||||||
|         cur.execute(self.SQL_CREATE_TABLE) |  | ||||||
|         conn.commit() |  | ||||||
|         cur.close() |  | ||||||
|         conn.close() |  | ||||||
|  |  | ||||||
|     def emit(self, record): |     def emit(self, record): | ||||||
|         conn = self.pool.get_connection() |         self.sql_execute("log_insert_entry.sql", | ||||||
|         cur = conn.cursor() |  | ||||||
|         cur.execute(self.SQL_INSERT_ENTRY, ( |  | ||||||
|             time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)), |             time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)), | ||||||
|             record.name, |             record.name, | ||||||
|             record.levelno, |             record.levelno, | ||||||
| @@ -39,7 +22,4 @@ class MySQLLogHandler(logging.Handler): | |||||||
|             logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "", |             logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "", | ||||||
|             record.process, |             record.process, | ||||||
|             record.thread, |             record.thread, | ||||||
|             record.threadName)) |             record.threadName) | ||||||
|         conn.commit() |  | ||||||
|         cur.close() |  | ||||||
|         conn.close() |  | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
|  |  | ||||||
| import click | import click | ||||||
| import json | import json | ||||||
|  | import logging | ||||||
| import requests | import requests | ||||||
|  | from datetime import datetime | ||||||
| from certidude import config | from certidude import config | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -9,13 +11,29 @@ def publish(event_type, event_data): | |||||||
|     """ |     """ | ||||||
|     Publish event on push server |     Publish event on push server | ||||||
|     """ |     """ | ||||||
|     if not isinstance(event_data, str): |     if not isinstance(event_data, basestring): | ||||||
|         from certidude.decorators import MyEncoder |         from certidude.decorators import MyEncoder | ||||||
|         event_data = json.dumps(event_data, cls=MyEncoder) |         event_data = json.dumps(event_data, cls=MyEncoder) | ||||||
|  |  | ||||||
|  |     url = config.PUSH_PUBLISH % config.PUSH_TOKEN | ||||||
|  |     click.echo("Publishing %s event %s on %s" % (event_type, event_data, url)) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|         notification = requests.post( |         notification = requests.post( | ||||||
|         config.PUSH_PUBLISH % config.PUSH_TOKEN, |             url, | ||||||
|             data=event_data, |             data=event_data, | ||||||
|             headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"}) |             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 os | ||||||
| import asyncore | import asyncore | ||||||
| import asynchat | import asynchat | ||||||
|  | from certidude import constants, config | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from OpenSSL import crypto | from OpenSSL import crypto | ||||||
|  |  | ||||||
| @@ -26,8 +27,6 @@ certificate authoirty (basicConstraints=CA:TRUE) or | |||||||
| TLS server certificates (extendedKeyUsage=serverAuth). | TLS server certificates (extendedKeyUsage=serverAuth). | ||||||
| """ | """ | ||||||
|  |  | ||||||
| EXTENSION_WHITELIST = set(["subjectAltName"]) |  | ||||||
|  |  | ||||||
| def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None): | def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None): | ||||||
|     """ |     """ | ||||||
|     Sign certificate signing request directly with private key assuming it's readable by the process |     Sign certificate signing request directly with private key assuming it's readable by the process | ||||||
| @@ -43,12 +42,6 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa | |||||||
|     # Set issuer |     # Set issuer | ||||||
|     cert.set_issuer(ca_cert.get_subject()) |     cert.set_issuer(ca_cert.get_subject()) | ||||||
|  |  | ||||||
|         # TODO: Assert openssl.cnf policy for subject attributes |  | ||||||
| #        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 |     # Copy attributes from CA | ||||||
|     if ca_cert.get_subject().C: |     if ca_cert.get_subject().C: | ||||||
|         cert.get_subject().C  = 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 |     # Copy attributes from request | ||||||
|     cert.get_subject().CN = request.get_subject().CN |     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 |         cert.get_subject().OU = req_subject.OU | ||||||
|  |  | ||||||
|     # Copy e-mail, key usage, extended key from request |     # 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( |     cert.set_serial_number(random.randint( | ||||||
|         0x1000000000000000000000000000000000000000, |         0x1000000000000000000000000000000000000000, | ||||||
|         0xffffffffffffffffffffffffffffffffffffffff)) |         0xffffffffffffffffffffffffffffffffffffffff)) | ||||||
|         cert.sign(private_key, 'sha1') |     cert.sign(private_key, 'sha256') | ||||||
|     return cert |     return cert | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -128,7 +126,7 @@ class SignHandler(asynchat.async_chat): | |||||||
|                     serial_number, timestamp = line.split(":") |                     serial_number, timestamp = line.split(":") | ||||||
|                     # TODO: Assert serial against regex |                     # TODO: Assert serial against regex | ||||||
|                     revocation = crypto.Revoked() |                     revocation = crypto.Revoked() | ||||||
|                     revocation.set_rev_date(datetime.fromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii")) |                     revocation.set_rev_date(datetime.utcfromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii")) | ||||||
|                     revocation.set_reason(b"keyCompromise") |                     revocation.set_reason(b"keyCompromise") | ||||||
|                     revocation.set_serial(serial_number.encode("ascii")) |                     revocation.set_serial(serial_number.encode("ascii")) | ||||||
|                     crl.add_revoked(revocation) |                     crl.add_revoked(revocation) | ||||||
| @@ -137,7 +135,7 @@ class SignHandler(asynchat.async_chat): | |||||||
|                 self.server.certificate, |                 self.server.certificate, | ||||||
|                 self.server.private_key, |                 self.server.private_key, | ||||||
|                 crypto.FILETYPE_PEM, |                 crypto.FILETYPE_PEM, | ||||||
|                 self.server.revocation_list_lifetime)) |                 config.REVOCATION_LIST_LIFETIME)) | ||||||
|  |  | ||||||
|         elif cmd == "ocsp-request": |         elif cmd == "ocsp-request": | ||||||
|             NotImplemented # TODO: Implement OCSP |             NotImplemented # TODO: Implement OCSP | ||||||
| @@ -147,7 +145,7 @@ class SignHandler(asynchat.async_chat): | |||||||
|  |  | ||||||
|             for e in request.get_extensions(): |             for e in request.get_extensions(): | ||||||
|                 key = e.get_short_name().decode("ascii") |                 key = e.get_short_name().decode("ascii") | ||||||
|                 if key not in EXTENSION_WHITELIST: |                 if key not in constants.EXTENSION_WHITELIST: | ||||||
|                     raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key) |                     raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key) | ||||||
|  |  | ||||||
|             # TODO: Potential exploits during PEM parsing? |             # TODO: Potential exploits during PEM parsing? | ||||||
| @@ -155,10 +153,10 @@ class SignHandler(asynchat.async_chat): | |||||||
|                 self.server.private_key, |                 self.server.private_key, | ||||||
|                 self.server.certificate, |                 self.server.certificate, | ||||||
|                 request, |                 request, | ||||||
|                 basic_constraints=self.server.basic_constraints, |                 basic_constraints=config.CERTIFICATE_BASIC_CONSTRAINTS, | ||||||
|                 key_usage=self.server.key_usage, |                 key_usage=config.CERTIFICATE_KEY_USAGE_FLAGS, | ||||||
|                 extended_key_usage=self.server.extended_key_usage, |                 extended_key_usage=config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS, | ||||||
|                 lifetime=self.server.lifetime) |                 lifetime=config.CERTIFICATE_LIFETIME) | ||||||
|             self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) |             self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) | ||||||
|         else: |         else: | ||||||
|             raise NotImplementedError("Unknown command: %s" % cmd) |             raise NotImplementedError("Unknown command: %s" % cmd) | ||||||
| @@ -175,26 +173,23 @@ class SignHandler(asynchat.async_chat): | |||||||
|  |  | ||||||
|  |  | ||||||
| class SignServer(asyncore.dispatcher): | class SignServer(asyncore.dispatcher): | ||||||
|     def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage, revocation_list_lifetime): |     def __init__(self): | ||||||
|         asyncore.dispatcher.__init__(self) |         asyncore.dispatcher.__init__(self) | ||||||
|  |  | ||||||
|         # Bind to sockets |         # Bind to sockets | ||||||
|         if os.path.exists(socket_path): |         if os.path.exists(config.SIGNER_SOCKET_PATH): | ||||||
|             os.unlink(socket_path) |             os.unlink(config.SIGNER_SOCKET_PATH) | ||||||
|         os.umask(0o007) |         os.umask(0o007) | ||||||
|         self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) |         self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||||
|         self.bind(socket_path) |         self.bind(config.SIGNER_SOCKET_PATH) | ||||||
|         self.listen(5) |         self.listen(5) | ||||||
|  |  | ||||||
|         # Load CA private key and certificate |  | ||||||
|         self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read()) |  | ||||||
|         self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read()) |  | ||||||
|         self.lifetime = lifetime |  | ||||||
|         self.revocation_list_lifetime = revocation_list_lifetime |  | ||||||
|         self.basic_constraints = basic_constraints |  | ||||||
|         self.key_usage = key_usage |  | ||||||
|         self.extended_key_usage = extended_key_usage |  | ||||||
|  |  | ||||||
|  |         # Load CA private key and certificate | ||||||
|  |         self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, | ||||||
|  |             open(config.AUTHORITY_PRIVATE_KEY_PATH).read()) | ||||||
|  |         self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, | ||||||
|  |             open(config.AUTHORITY_CERTIFICATE_PATH).read()) | ||||||
|  |  | ||||||
|         # Perhaps perform chroot as well, currently results in |         # Perhaps perform chroot as well, currently results in | ||||||
|         # (<class 'OpenSSL.crypto.Error'>:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded') |         # (<class 'OpenSSL.crypto.Error'>:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded') | ||||||
|   | |||||||
							
								
								
									
										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%; |     max-height: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
| ul { |  | ||||||
|     list-style: none; |  | ||||||
|     margin: 1em 0; |  | ||||||
|     padding: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #pending_requests .notify { | #pending_requests .notify { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| @@ -142,7 +136,17 @@ pre { | |||||||
|     margin: 0 auto; |     margin: 0 auto; | ||||||
| } | } | ||||||
|  |  | ||||||
| #container li { | #signed ul, | ||||||
|  | #requests ul, | ||||||
|  | #log ul { | ||||||
|  |     list-style: none; | ||||||
|  |     margin: 1em 0; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #signed li, | ||||||
|  | #requests li, | ||||||
|  | #log li { | ||||||
|     margin: 4px 0; |     margin: 4px 0; | ||||||
|     padding: 4px 0; |     padding: 4px 0; | ||||||
|     clear: both; |     clear: both; | ||||||
| @@ -164,7 +168,8 @@ pre { | |||||||
|  |  | ||||||
| .icon{ | .icon{ | ||||||
|     background-size: 24px; |     background-size: 24px; | ||||||
|     padding-left: 36px; |     background-position: 6px 2px; | ||||||
|  |     padding-left: 32px; | ||||||
|     background-repeat: no-repeat; |     background-repeat: no-repeat; | ||||||
|     display: block; |     display: block; | ||||||
|     vertical-align: text-bottom; |     vertical-align: text-bottom; | ||||||
| @@ -172,7 +177,7 @@ pre { | |||||||
| } | } | ||||||
|  |  | ||||||
| #log_entries li span.icon { | #log_entries li span.icon { | ||||||
|     background-size: 32px; |     background-size: 24px; | ||||||
|     padding-left: 42px; |     padding-left: 42px; | ||||||
|     padding-top: 2px; |     padding-top: 2px; | ||||||
|     padding-bottom: 2px; |     padding-bottom: 2px; | ||||||
| @@ -180,7 +185,8 @@ pre { | |||||||
|  |  | ||||||
| .tags .tag { | .tags .tag { | ||||||
|     display: inline; |     display: inline; | ||||||
|     background-size: 32px; |     background-size: 24px; | ||||||
|  |     background-position: 0 4px; | ||||||
|     padding-top: 4px; |     padding-top: 4px; | ||||||
|     padding-bottom: 4px; |     padding-bottom: 4px; | ||||||
|     padding-right: 1em; |     padding-right: 1em; | ||||||
| @@ -199,25 +205,25 @@ select { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); } | .icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); } | ||||||
|  |  | ||||||
| .icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); } | .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } | ||||||
| .icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); } | .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } | ||||||
| .icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); } | .icon.warning { background-image: url("../img/iconmonstr-warning-8.svg"); } | ||||||
| .icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); } | .icon.info { background-image: url("../img/iconmonstr-info-8.svg"); } | ||||||
|  |  | ||||||
| .icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); } | .icon.revoke { background-image: url("../img/iconmonstr-x-mark-8.svg"); } | ||||||
| .icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); } | .icon.download { background-image: url("../img/iconmonstr-download-12.svg"); } | ||||||
| .icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); } | .icon.sign { background-image: url("../img/iconmonstr-pen-14.svg"); } | ||||||
| .icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); } | .icon.search { background-image: url("../img/iconmonstr-magnifier-4.svg"); } | ||||||
|  |  | ||||||
| .icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); } | .icon.phone { background-image: url("../img/iconmonstr-mobile-phone-7.svg"); } | ||||||
| .icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); } | .icon.location { background-image: url("../img/iconmonstr-compass-7.svg"); } | ||||||
| .icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); } | .icon.room { background-image: url("../img/iconmonstr-home-7.svg"); } | ||||||
| .icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); } | .icon.serial { background-image: url("../img/iconmonstr-barcode-4.svg"); } | ||||||
|  |  | ||||||
| .icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); } | .icon.wireless { background-image: url("../img/iconmonstr-wireless-6.svg"); } | ||||||
| .icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); } | .icon.password { background-image: url("../img/iconmonstr-lock-3.svg"); } | ||||||
|  |  | ||||||
| /* Make sure this is the last one */ | /* Make sure this is the last one */ | ||||||
| .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} | .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} | ||||||
|   | |||||||
| @@ -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> | <body> | ||||||
|     <nav id="menu"> |     <nav id="menu"> | ||||||
|         <ul class="container"> |         <ul class="container"> | ||||||
|           <li data-section="requests">Requests</li> |           <li data-section="about">Profile</li> | ||||||
|           <li data-section="signed">Signed</li> |           <li id="section-requests" data-section="requests" style="display:none;">Requests</li> | ||||||
|           <li data-section="revoked">Revoked</li> |           <li id="section-signed" data-section="signed" style="display:none;">Signed</li> | ||||||
|           <li data-section="config">Configuration</li> |           <li id="section-revoked" data-section="revoked" style="display:none;">Revoked</li> | ||||||
|           <li data-section="log">Log</li> |           <li id="section-config" data-section="config" style="display:none;">Configuration</li> | ||||||
|  |           <li id="section-log" data-section="log" style="display:none;">Log</li> | ||||||
|         </ul> |         </ul> | ||||||
|     </nav> |     </nav> | ||||||
|     <div id="container" class="container"> |     <div id="container" class="container"> | ||||||
|   | |||||||
| @@ -67,7 +67,6 @@ function onRequestSubmitted(e) { | |||||||
|         url: "/api/request/" + e.data + "/", |         url: "/api/request/" + e.data + "/", | ||||||
|         dataType: "json", |         dataType: "json", | ||||||
|         success: function(request, status, xhr) { |         success: function(request, status, xhr) { | ||||||
|             console.info(request); |  | ||||||
|             $("#pending_requests").prepend( |             $("#pending_requests").prepend( | ||||||
|                 nunjucks.render('views/request.html', { request: request })); |                 nunjucks.render('views/request.html', { request: request })); | ||||||
|         } |         } | ||||||
| @@ -75,12 +74,12 @@ function onRequestSubmitted(e) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onRequestDeleted(e) { | function onRequestDeleted(e) { | ||||||
|     console.log("Removing deleted request #" + e.data); |     console.log("Removing deleted request", e.data); | ||||||
|     $("#request_" + e.data).remove(); |     $("#request-" + e.data.replace("@", "--").replace(".", "-")).remove(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onClientUp(e) { | function onClientUp(e) { | ||||||
|     console.log("Adding security association:" + e.data); |     console.log("Adding security association:", e.data); | ||||||
|     var lease = JSON.parse(e.data); |     var lease = JSON.parse(e.data); | ||||||
|     var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); |     var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); | ||||||
|     $status.html(nunjucks.render('views/status.html', { |     $status.html(nunjucks.render('views/status.html', { | ||||||
| @@ -93,7 +92,7 @@ function onClientUp(e) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onClientDown(e) { | function onClientDown(e) { | ||||||
|     console.log("Removing security association:" + e.data); |     console.log("Removing security association:", e.data); | ||||||
|     var lease = JSON.parse(e.data); |     var lease = JSON.parse(e.data); | ||||||
|     var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); |     var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); | ||||||
|     $status.html(nunjucks.render('views/status.html', { |     $status.html(nunjucks.render('views/status.html', { | ||||||
| @@ -107,7 +106,9 @@ function onClientDown(e) { | |||||||
|  |  | ||||||
| function onRequestSigned(e) { | function onRequestSigned(e) { | ||||||
|     console.log("Request signed:", e.data); |     console.log("Request signed:", e.data); | ||||||
|     $("#request_" + e.data).slideUp("normal", function() { $(this).remove(); }); |  | ||||||
|  |     $("#request-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); | ||||||
|  |     $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); | ||||||
|  |  | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "GET", |         method: "GET", | ||||||
| @@ -121,13 +122,14 @@ function onRequestSigned(e) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| function onCertificateRevoked(e) { | function onCertificateRevoked(e) { | ||||||
|     console.log("Removing revoked certificate #" + e.data); |     console.log("Removing revoked certificate", e.data); | ||||||
|     $("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); }); |     $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagAdded(e) { | function onTagAdded(e) { | ||||||
|     console.log("Tag added #" + e.data); |     console.log("Tag added", e.data); | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "GET", |         method: "GET", | ||||||
|         url: "/api/tag/" + e.data + "/", |         url: "/api/tag/" + e.data + "/", | ||||||
| @@ -143,12 +145,12 @@ function onTagAdded(e) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onTagRemoved(e) { | function onTagRemoved(e) { | ||||||
|     console.log("Tag removed #" + e.data); |     console.log("Tag removed", e.data); | ||||||
|     $("#tag_" + e.data).remove(); |     $("#tag_" + e.data).remove(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagUpdated(e) { | function onTagUpdated(e) { | ||||||
|     console.log("Tag updated #" + e.data); |     console.log("Tag updated", e.data); | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "GET", |         method: "GET", | ||||||
|         url: "/api/tag/" + e.data + "/", |         url: "/api/tag/" + e.data + "/", | ||||||
| @@ -175,9 +177,26 @@ $(document).ready(function() { | |||||||
|             $("#container").html(nunjucks.render('views/error.html', { message: msg })); |             $("#container").html(nunjucks.render('views/error.html', { message: msg })); | ||||||
|         }, |         }, | ||||||
|         success: function(session, status, xhr) { |         success: function(session, status, xhr) { | ||||||
|             console.info("Opening EventSource from:", session.event_channel); |             $("#login").hide(); | ||||||
|  |  | ||||||
|             var source = new EventSource(session.event_channel); |             /** | ||||||
|  |              * 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) { |                 source.onmessage = function(event) { | ||||||
|                     console.log("Received server-sent event:", event); |                     console.log("Received server-sent event:", event); | ||||||
| @@ -194,13 +213,13 @@ $(document).ready(function() { | |||||||
|                 source.addEventListener("tag-removed", onTagRemoved); |                 source.addEventListener("tag-removed", onTagRemoved); | ||||||
|                 source.addEventListener("tag-updated", onTagUpdated); |                 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"); |                 console.info("Swtiching to requests section"); | ||||||
|                 $("section").hide(); |                 $("section").hide(); | ||||||
|                 $("section#requests").show(); |                 $("section#requests").show(); | ||||||
|  |                 $("#section-revoked").show(); | ||||||
|  |                 $("#section-signed").show(); | ||||||
|  |                 $("#section-requests").show(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             $("nav#menu li").click(function(e) { |             $("nav#menu li").click(function(e) { | ||||||
|                 $("section").hide(); |                 $("section").hide(); | ||||||
| @@ -231,14 +250,16 @@ $(document).ready(function() { | |||||||
|  |  | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  |             console.log("Features enabled:", session.features); | ||||||
|  |             if (session.features.tagging) { | ||||||
|  |                 console.info("Tagging enabled"); | ||||||
|  |                 $("#section-config").show(); | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     method: "GET", |                     method: "GET", | ||||||
|                     url: "/api/config/", |                     url: "/api/config/", | ||||||
|                     dataType: "json", |                     dataType: "json", | ||||||
|                     success: function(configuration, status, xhr) { |                     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})); |                         $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); | ||||||
|                         /** |                         /** | ||||||
|                          * Fetch tags for certificates |                          * Fetch tags for certificates | ||||||
| @@ -249,6 +270,7 @@ $(document).ready(function() { | |||||||
|                             dataType: "json", |                             dataType: "json", | ||||||
|                             success:function(tags, status, xhr) { |                             success:function(tags, status, xhr) { | ||||||
|                                 console.info("Got", tags.length, "tags"); |                                 console.info("Got", tags.length, "tags"); | ||||||
|  |                                 $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); | ||||||
|                                 for (var j = 0; j < tags.length; j++) { |                                 for (var j = 0; j < tags.length; j++) { | ||||||
|                                     // TODO: Deduplicate |                                     // 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>"); |                                     $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 |              * Fetch leases associated with certificates | ||||||
|              */ |              */ | ||||||
|  |             if (session.features.leases) { | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     method: "GET", |                     method: "GET", | ||||||
|                     url: "/api/lease/", |                     url: "/api/lease/", | ||||||
| @@ -290,10 +314,13 @@ $(document).ready(function() { | |||||||
|  |  | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             return; |             } | ||||||
|  |  | ||||||
|             /** |             /** | ||||||
|              * Fetch log entries |              * Fetch log entries | ||||||
|              */ |              */ | ||||||
|  |             if (session.features.logging) { | ||||||
|  |                 $("#section-log").show(); | ||||||
|                 $.ajax({ |                 $.ajax({ | ||||||
|                     method: "GET", |                     method: "GET", | ||||||
|                     url: "/api/log/", |                     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) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"barcode-4-icon\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M117,331.114V181.956h23.215v149.158H117z M162.318,331.114\r\n\tV181.956h27.75v149.158H162.318z M211.979,331.114V181.956h13.611v149.16L211.979,331.114z M297.614,331.114V181.956h13.61v149.16\r\n\tL297.614,331.114z M247.827,331.114l-0.001-149.158h27.749v149.16L247.827,331.114z M333.566,331.114v-149.16h23.217v149.16H333.566\r\n\tz M377.978,331.114v-149.16L395,181.956v149.158H377.978z M165.095,141.45H241v-30h-75.905V141.45z M345.566,111.45H269v30h76.566\r\n\tV111.45z M241,400.55v-30h-75.905v30H241z M462,224.863h-30v68h30V224.863z M373.566,141.45H432v55.413h30V111.45h-88.434V141.45z\r\n\t M345.566,370.55H269v30h76.566V370.55z M137.095,370.55H80v-49.687H50v79.687h87.095V370.55z M432,320.863v49.687h-58.434v30H462\r\n\tv-79.687H432z M50,292.863h30v-76H50V292.863z M80,188.863V141.45h57.095v-30H50v77.413H80z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 16v-8h2v8h-2zm12 0v-8h2v8h-2zm-9 0v-8h1v8h-1zm2 0v-8h2v8h-2zm3 0v-8h1v8h-1zm2 0v-8h1v8h-1zm5 0v-8h1v8h-1zm1-10h2v2h2v-4h-4v2zm-18 2v-2h2v-2h-4v4h2zm2 10h-2v-2h-2v4h4v-2zm18-2v2h-2v2h4v-4h-2zm-20-6h-2v4h2v-4zm22 0h-2v4h2v-4zm-13-6h-5v2h5v-2zm7 0h-5v2h5v-2zm-7 14h-5v2h5v-2zm7 0h-5v2h5v-2z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -22,14 +22,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-calendar-6.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" style=\"enable-background:new 0 0 512 512;\" xml:space=\"preserve\">\r\n<path id=\"certificate-15\" d=\"M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727\r\n\tc-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294\r\n\tc4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672\r\n\tc-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639\r\n\tc4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z\r\n\t M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572\r\n\tc-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677\r\n\tc-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564\r\n\tc0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649\r\n\tc12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564\r\n\tc4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292\r\n\tc-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95\r\n\tc-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z\r\n\t M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797\r\n\tC346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799\r\n\tc-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352\r\n\tc29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119\r\n\tC294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6\r\n\tc0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259\r\n\tc-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028\r\n\tc-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617\r\n\tl94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49\r\n\tc9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67\r\n\tL243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245\r\n\tc-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5\r\n\tL227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905\r\n\tl10.713,13.597l86.679-51.048L281.516,237.182z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 2v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2zm1 11.729l.855-.791c1 .484 1.635.852 2.76 1.654 2.113-2.399 3.511-3.616 6.106-5.231l.279.64c-2.141 1.869-3.709 3.949-5.967 7.999-1.393-1.64-2.322-2.686-4.033-4.271z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -46,14 +46,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"compass-7-icon\" d=\"M256,90c91.74,0,166,74.243,166,166c0,91.741-74.245,166-166,166c-91.741,0-166-74.245-166-166\r\n\tC90,164.259,164.244,90,256,90 M256,50C142.229,50,50,142.229,50,256s92.229,206,206,206s206-92.229,206-206S369.771,50,256,50z\r\n\t M197.686,216.466l-28.355-47.135l47.225,28.408C209.145,202.733,202.736,209.099,197.686,216.466z M296.709,198.612\r\n\tc6.459,4.562,12.119,10.179,16.729,16.602l29.232-45.883L296.709,198.612z M198.312,297.179l-28.982,45.492l45.416-28.936\r\n\tC208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74\r\n\tC309.596,303.253,303.299,309.593,296.018,314.604z M400.199,256.001l-99.238,21.998c-4.369,8.913-11.312,16.328-19.859,21.295\r\n\tL256,400.2l-25.104-100.908c-8.545-4.965-15.488-12.381-19.857-21.293l-99.238-21.998l99.238-21.999\r\n\tc4.369-8.913,11.312-16.328,19.857-21.294L256,111.8l25.104,100.908c8.545,4.966,15.488,12.381,19.857,21.294L400.199,256.001z\r\n\t M278.406,256c0-12.374-10.031-22.407-22.406-22.407S233.592,243.626,233.592,256c0,12.376,10.033,22.408,22.408,22.408\r\n\tS278.406,268.376,278.406,256z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M18.625 19.46c-.264 1.696-.97 3.247-2.1 4.54-.065-.461-.254-.908-.433-1.266-.409.216-.899.33-1.328.33l-.265-.016c1.199-1.25 1.791-2.544 1.965-4.27.281.079.623.12 1.053.12.415.284.578.46 1.108.562zm4.875 3.589c-1.197-1.248-1.782-2.549-1.957-4.271-.283.079-.625.122-1.061.122-.414.285-.587.461-1.106.561.264 1.697.97 3.247 2.099 4.54.065-.461.254-.908.433-1.266.51.269 1.131.372 1.592.314zm-4.992-4.829c-.704-.494-.552-.447-1.423-.444h-.002c-.362 0-.685-.225-.794-.557-.267-.797-.176-.673-.879-1.163-.302-.208-.418-.577-.307-.9.273-.793.273-.641 0-1.438-.109-.32.002-.691.307-.9.703-.488.611-.365.879-1.163.109-.333.432-.557.794-.557h.002c.87.002.718.052 1.423-.444.146-.102.318-.154.492-.154s.346.052.492.154c.704.495.552.447 1.423.444h.001c.363 0 .686.224.797.557.266.796.172.673.877 1.163.223.153.348.397.348.65l-.042.25c-.272.793-.272.641 0 1.438.111.317 0 .69-.306.9-.705.489-.611.366-.877 1.163-.205.614-.656.555-1.18.555-.463 0-.465.042-1.041.446-.293.207-.691.207-.984 0zm.492-1.814c1.088 0 1.969-.882 1.969-1.969 0-1.087-.881-1.969-1.969-1.969s-1.969.881-1.969 1.969c0 1.087.881 1.969 1.969 1.969zm-4.674 1.333c-1.675 1.058-3.561 2.247-3.952 2.493-.043-.772-.329-1.492-.828-2.084-.058-.074-5.813-7.4-7.222-9.204-1.109-1.42-.06-2.934 1.23-2.934 1.694 0 2.369 2.207.894 3.163l1.163 1.486 8.053-4.551c1.396-1.032 1.79-2.938.914-4.434-.605-1.032-1.726-1.674-2.924-1.674-.475 0-.936.098-1.373.292l-8.264 4.227c-1.301.624-2.017 1.846-2.017 3.147 0 .816.282 1.663.877 2.412 1.444 1.815 7.261 9.253 7.319 9.328.862 1.147.017 2.753-1.376 2.753-.989 0-1.516-.705-1.667-1.308-.176-.705.069-1.407.773-1.858l-1.141-1.458c-.855.602-1.4 1.515-1.507 2.536-.228 2.174 1.504 3.929 3.557 3.929.63 0 1.255-.174 1.807-.502l5.485-3.458c.264-.415.529-1.431.199-2.301zm-3.319-15.755c.203-.095.431-.145.659-.145.577 0 1.082.305 1.349.816.338.645.244 1.59-.594 2.071l-5.307 3.006c0-1.191-.581-2.284-1.57-2.952l5.463-2.796zm1.987 6.267l1.725 2.117c.348-.525.858-.921 1.46-1.121l-1.562-1.916-1.623.92zm-2.886 8.043l2.871-1.628-.633-.825-2.863 1.661.625.792zm1.842-6.987l-5.012 2.943.624.792 5.026-2.951-.638-.784zm1.286 1.597l-5.035 2.965.624.792 5.049-2.974-.638-.783z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -70,14 +70,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"download-12-icon\" d=\"M462,246.575c0,44.318-35.928,80.246-80.246,80.246H331.58v-38.119\n\n\tc-0.998-43.379,40.92-44.379,59.67-46.379c-27.168-33.334-70.918-48.244-104.611-48.244c-66.546,0-108.17,39.104-108.17,104.808\n\n\tv27.935h-48.223C85.928,326.821,50,290.894,50,246.575c0-40.982,30.729-74.766,70.396-79.623\n\n\tc2.891-42.287,49.035-66.355,85.217-45.898c19.236-30.605,53.297-50.953,92.115-50.953c57.107,0,103.932,44.033,108.375,100\n\n\tC438.516,180.413,462,210.747,462,246.575z M301.58,288.702c0-30.761,6.053-48.484,31.926-56.837\n\n\tc-20.066-8.452-125.037-28.815-125.037,67.021c0,32.187,0,58.909,0,58.909h-37.408l83.963,84.104l83.965-84.104H301.58\n\n\tC301.58,357.796,301.58,315.59,301.58,288.702z\"/>\n\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.608 9.476l-1.608-5.476-1.611 5.477c-.429.275-.775.658-1.019 1.107l-5.37 1.416 5.37 1.416c.243.449.589.833 1.019 1.107l1.611 5.477 1.618-5.479c.428-.275.771-.659 1.014-1.109l5.368-1.412-5.368-1.413c-.244-.452-.592-.836-1.024-1.111zm-1.608 4.024c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm5.25 3.75l-2.573-1.639c.356-.264.67-.579.935-.934l1.638 2.573zm-2.641-8.911l2.64-1.588-1.588 2.639c-.29-.407-.645-.761-1.052-1.051zm-5.215 7.325l-2.644 1.586 1.589-2.641c.29.408.646.764 1.055 1.055zm-1.005-6.34l-1.638-2.573 2.573 1.638c-.357.264-.672.579-.935.935z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -94,14 +94,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"email-2-icon\" d=\"M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465\n\n\tL96.473,138.407H415.533z M84.744,173.506l85.504,65.441L84.744,324.45V173.506z M85.1,373.593l113.186-113.186l57.654,44.127\n\n\tl57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z\"/>\n\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M6 13h4v-7h4v7h4l-6 6-6-6zm16-1c0 5.514-4.486 10-10 10s-10-4.486-10-10 4.486-10 10-10 10 4.486 10 10zm2 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -118,14 +118,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"error-4-icon\" d=\"M324.76,90L422,187.24v137.52L324.76,422H187.24L90,324.76V187.24L187.24,90H324.76 M341.328,50H170.672\n\tL50,170.672v170.656L170.672,462h170.656L462,341.328V170.672L341.328,50L341.328,50z M228.55,135.812h54.9v166.5h-54.9V135.812z\n\t M256,388.188c-16.362,0-29.625-13.264-29.625-29.625c0-16.362,13.263-29.627,29.625-29.627c16.361,0,29.625,13.265,29.625,29.627\n\tC285.625,374.924,272.361,388.188,256,388.188z\"/>\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -142,14 +142,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"flag-3-icon\" d=\"M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642\r\n\tc-60.271,0-61.627-51.923-131.596-51.923c-37.832,0-73.106,17.577-88.045,30.381c0,12.64,0,216.762,0,216.762\r\n\tc21.204-14.696,53.426-30.144,88.286-30.144c66.08,0,75.343,49.388,134.242,49.388c38.042,0,64.437-24.369,64.437-24.369V80.746z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M16.143 2l5.857 5.858v8.284l-5.857 5.858h-8.286l-5.857-5.858v-8.284l5.857-5.858h8.286zm.828-2h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-6.471 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -166,14 +166,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-4-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"home-4-icon\" d=\"M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492\n\n\tz M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z\"/>\n\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 24h-2v-24h2v24zm18-21.387s-1.621 1.43-3.754 1.43c-3.36 0-3.436-2.895-7.337-2.895-2.108 0-4.075.98-4.909 1.694v12.085c1.184-.819 2.979-1.681 4.923-1.681 3.684 0 4.201 2.754 7.484 2.754 2.122 0 3.593-1.359 3.593-1.359v-12.028z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -190,14 +190,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-6-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-7.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"info-6-icon\" d=\"M256,90.002c91.74,0,166,74.241,166,165.998c0,91.739-74.245,165.998-166,165.998\n\tc-91.738,0-166-74.242-166-165.998C90,164.259,164.243,90.002,256,90.002 M256,50.002C142.229,50.002,50,142.228,50,256\n\tc0,113.769,92.229,205.998,206,205.998c113.77,0,206-92.229,206-205.998C462,142.228,369.77,50.002,256,50.002L256,50.002z\n\t M252.566,371.808c-28.21,9.913-51.466-1.455-46.801-28.547c4.667-27.098,31.436-85.109,35.255-96.079\n\tc3.816-10.97-3.502-13.977-11.346-9.513c-4.524,2.61-11.248,7.841-17.02,12.925c-1.601-3.223-3.852-6.906-5.542-10.433\n\tc9.419-9.439,25.164-22.094,43.803-26.681c22.27-5.497,59.492,3.29,43.494,45.858c-11.424,30.34-19.503,51.276-24.594,66.868\n\tc-5.088,15.598,0.955,18.868,9.863,12.791c6.959-4.751,14.372-11.214,19.806-16.226c2.515,4.086,3.319,5.389,5.806,10.084\n\tC295.857,342.524,271.182,365.151,252.566,371.808z M311.016,184.127c-12.795,10.891-31.76,10.655-42.37-0.532\n\tc-10.607-11.181-8.837-29.076,3.955-39.969c12.794-10.89,31.763-10.654,42.37,0.525\n\tC325.577,155.337,323.809,173.231,311.016,184.127z\"/>\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M20 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -214,14 +214,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-2-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-8.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"key-2-icon\" stroke=\"#000000\" stroke-miterlimit=\"10\" d=\"M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872\r\n\tl-45.746-0.001v41.345l-100.004-0.001l150.078-150.076c-4.578-4.686-10.061-11.391-13.691-17.423L50,426.498v-40.939\r\n\tl145.736-145.736C212.174,278.996,244.713,310.705,286.529,325.486z M425.646,92.339c48.473,48.473,48.471,127.064-0.002,175.535\r\n\tc-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537\r\n\tC298.58,43.865,377.172,43.865,425.646,92.339z M400.73,117.165c-12.023-12.021-31.516-12.021-43.537,0\r\n\tc-12.021,12.022-12.021,31.517,0,43.538s31.514,12.021,43.537-0.001C412.754,148.68,412.75,129.188,400.73,117.165z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.033 16.01c.564-1.789 1.632-3.932 1.821-4.474.273-.787-.211-1.136-1.74.209l-.34-.64c1.744-1.897 5.335-2.326 4.113.613-.763 1.835-1.309 3.074-1.621 4.03-.455 1.393.694.828 1.819-.211.153.25.203.331.356.619-2.498 2.378-5.271 2.588-4.408-.146zm4.742-8.169c-.532.453-1.32.443-1.761-.022-.441-.465-.367-1.208.164-1.661.532-.453 1.32-.442 1.761.022.439.466.367 1.209-.164 1.661z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -238,14 +238,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-lock-3-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-3.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"lock-3-icon\" d=\"M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667\r\n\tv62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108\r\n\tV253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773\r\n\tc4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.451 17.337l-2.451 2.663h-2v2h-2v2h-6v-1.293l7.06-7.06c-.214-.26-.413-.533-.599-.815l-6.461 6.461v-2.293l6.865-6.949c1.08 2.424 3.095 4.336 5.586 5.286zm11.549-9.337c0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8 8 3.582 8 8zm-3-3c0-1.104-.896-2-2-2s-2 .896-2 2 .896 2 2 2 2-.896 2-2z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -262,14 +262,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"magnifier-4-icon\" d=\"M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094\n\n\tc0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641\n\n\tc31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764\n\n\tc0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352\n\n\tS103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862\n\n\tC265.221,150.329,188.719,144.834,138.455,188.504z\"/>\n\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M23.111 20.058l-4.977-4.977c.965-1.52 1.523-3.322 1.523-5.251 0-5.42-4.409-9.83-9.829-9.83-5.42 0-9.828 4.41-9.828 9.83s4.408 9.83 9.829 9.83c1.834 0 3.552-.505 5.022-1.383l5.021 5.021c2.144 2.141 5.384-1.096 3.239-3.24zm-20.064-10.228c0-3.739 3.043-6.782 6.782-6.782s6.782 3.042 6.782 6.782-3.043 6.782-6.782 6.782-6.782-3.043-6.782-6.782zm2.01-1.764c1.984-4.599 8.664-4.066 9.922.749-2.534-2.974-6.993-3.294-9.922-.749z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -286,14 +286,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-6-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-7.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"mobile-phone-6-icon\" d=\"M139.59,131.775c-13.807,0-25,11.197-25,25.01V436.99c0,13.812,11.193,25.01,25,25.01h150.49\r\n\tc13.807,0,25-11.198,25-25.01V156.766c0-13.802-11.186-24.99-24.98-24.99H139.59z M179.832,416.514h-30.996v-24.51h30.996V416.514z\r\n\t M179.832,372.203h-30.996v-24.51h30.996V372.203z M230.334,416.514h-30.996v-24.51h30.996V416.514z M230.334,372.203h-30.996\r\n\tv-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z\r\n\t M280.836,312.887h-132V183.226h132V312.887z M283.451,111.408c13.445-0.01,26.9,5.113,37.164,15.369s15.4,23.699,15.41,37.147\r\n\th22.121c-0.012-19.113-7.312-38.231-21.898-52.805c-14.588-14.573-33.691-21.854-52.797-21.842V111.408z M283.451,72.682\r\n\tc23.354-0.015,46.691,8.882,64.52,26.696c17.828,17.812,26.75,41.187,26.766,64.547h22.674c-0.02-29.166-11.16-58.358-33.418-80.597\r\n\tC341.734,61.089,312.605,49.982,283.451,50V72.682z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M5 6c-1.104 0-2 .896-2 2v14c0 1.104.896 2 2 2h8c1.104 0 2-.896 2-2v-14c0-1.104-.896-2-2-2h-8zm2 15h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm0-3h-8v-7h8v7zm0-11.688c.944-.001 1.889.359 2.608 1.08.721.72 1.082 1.664 1.082 2.606h1.554c-.001-1.341-.514-2.684-1.538-3.707-1.025-1.022-2.365-1.533-3.706-1.532v1.553zm0-2.718c1.639-.001 3.277.623 4.53 1.874 1.251 1.25 1.877 2.892 1.878 4.531h1.592c-.001-2.047-.782-4.096-2.345-5.658-1.562-1.562-3.609-2.341-5.655-2.34v1.593z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -310,14 +310,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-10-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-14.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"pen-10-icon\" d=\"M244.558,199.493l67.827,67.826l-73.17,134.531c0,0-90.805,23.4-147.694,60.027l-14.185-14.182\r\n\tl68.113-68.105c5.975-5.982,13.726-9.773,22.11-10.807c4.642-0.582,9.128-2.621,12.696-6.205c8.538-8.547,8.546-22.4-0.002-30.951\r\n\tc-8.549-8.543-22.407-8.543-30.959-0.002c-3.573,3.572-5.623,8.061-6.199,12.693c-1.028,8.371-4.834,16.15-10.8,22.117\r\n\tl-68.104,68.105L50,420.354c37.028-57.496,60.021-147.693,60.021-147.693L244.558,199.493z M315.896,50.122\r\n\tc-22.784,44.143-53.014,100-53.014,100l98.872,98.869c0,0,55.909-30.086,100.246-52.766L315.896,50.122z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.014 6.54s2.147-3.969 3.475-6.54l8.511 8.511c-2.583 1.321-6.556 3.459-6.556 3.459l-5.43-5.43zm-8.517 6.423s-1.339 5.254-3.497 8.604l.827.826 3.967-3.967c.348-.348.569-.801.629-1.288.034-.27.153-.532.361-.74.498-.498 1.306-.498 1.803 0 .498.499.498 1.305 0 1.803-.208.209-.469.328-.74.361-.488.061-.94.281-1.288.63l-3.967 3.968.826.84c3.314-2.133 8.604-3.511 8.604-3.511l4.262-7.837-3.951-3.951-7.836 4.262z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -334,14 +334,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-2-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-3.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"tag-2-icon\" d=\"M234.508,50L50.068,50.262l-0.004,184.311L277.365,462l184.57-184.57L234.508,50z M114.877,167.365\n\n\tc-15.027-15.027-15.027-39.395,0-54.424c15.029-15.029,39.396-15.029,54.426,0s15.029,39.396,0,54.424\n\n\tC154.273,182.395,129.906,182.395,114.877,167.365z M242.316,327.94l-76.225-76.226l17.678-17.678l76.225,76.226L242.316,327.94z\n\n\t M317.609,335.887L199.764,218.041l17.678-17.678l117.846,117.846L317.609,335.887z M351.818,301.678L233.973,183.832l17.678-17.678\n\n\tL369.496,284L351.818,301.678z\"/>\n\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M10.605 0h-10.604v10.609l13.39 13.391 10.609-10.605-13.395-13.395zm-7.019 6.414c-.781-.782-.781-2.047 0-2.828.782-.781 2.048-.781 2.828-.002.782.783.782 2.048 0 2.83-.781.781-2.046.781-2.828 0zm6.823 8.947l-4.243-4.242.708-.708 4.243 4.243-.708.707zm4.949.707l-7.07-7.071.707-.707 7.071 7.071-.708.707zm2.121-2.121l-7.071-7.071.707-.707 7.071 7.071-.707.707z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -358,14 +358,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-time-13-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-user-5.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"time-13-icon\" d=\"M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933\r\n\tc0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13\r\n\tc-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13\r\n\tc-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096\r\n\tc-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202\r\n\tc-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13\r\n\tc7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z\r\n\t M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5\r\n\tH197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M19 7.001c0 3.865-3.134 7-7 7s-7-3.135-7-7c0-3.867 3.134-7.001 7-7.001s7 3.134 7 7.001zm-1.598 7.18c-1.506 1.137-3.374 1.82-5.402 1.82-2.03 0-3.899-.685-5.407-1.822-4.072 1.793-6.593 7.376-6.593 9.821h24c0-2.423-2.6-8.006-6.598-9.819z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -382,14 +382,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-6-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-8.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"warning-6-icon\" d=\"M239.939,231.352h32.121v97.421h-32.121V231.352z M256,379.019c-9.574,0-17.334-7.761-17.334-17.334\n\tc0-9.574,7.76-17.335,17.334-17.335c9.573,0,17.334,7.761,17.334,17.335C273.334,371.258,265.573,379.019,256,379.019z M256,78.07\n\tL50,434.873h412L256,78.07z M256,158.07l136.718,236.803H119.282L256,158.07z\"/>\n</svg>\n\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 5.177l8.631 15.823h-17.262l8.631-15.823zm0-4.177l-12 22h24l-12-22zm-1 9h2v6h-2v-6zm1 9.75c-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25 1.25.56 1.25 1.25-.561 1.25-1.25 1.25z\"/></svg>"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -406,38 +406,14 @@ root: root | |||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-wireless-6-icon.svg"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-8.svg"] = (function() { | ||||||
| function root(env, context, frame, runtime, cb) { | function root(env, context, frame, runtime, cb) { | ||||||
| var lineno = null; | var lineno = null; | ||||||
| var colno = null; | var colno = null; | ||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"wireless-6-icon\" d=\"M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35\r\n\tc-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796\r\n\tc27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936\r\n\tc-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599\r\n\tc11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269\r\n\ts-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297\r\n\tc0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z\"/>\r\n</svg>\r\n"; | output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 3.752l-4.423-3.752-7.771 9.039-7.647-9.008-4.159 4.278c2.285 2.885 5.284 5.903 8.362 8.708l-8.165 9.447 1.343 1.487c1.978-1.335 5.981-4.373 10.205-7.958 4.304 3.67 8.306 6.663 10.229 8.006l1.449-1.278-8.254-9.724c3.287-2.973 6.584-6.354 8.831-9.245z\"/></svg>"; | ||||||
| if(parentTemplate) { |  | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); |  | ||||||
| } else { |  | ||||||
| cb(null, output); |  | ||||||
| } |  | ||||||
| ; |  | ||||||
| } catch (e) { |  | ||||||
|   cb(runtime.handleError(e, lineno, colno)); |  | ||||||
| } |  | ||||||
| } |  | ||||||
| return { |  | ||||||
| root: root |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| })(); |  | ||||||
| })(); |  | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-5-icon.svg"] = (function() { |  | ||||||
| function root(env, context, frame, runtime, cb) { |  | ||||||
| var lineno = null; |  | ||||||
| var colno = null; |  | ||||||
| var output = ""; |  | ||||||
| try { |  | ||||||
| var parentTemplate = null; |  | ||||||
| output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"x-mark-5-icon\" d=\"M432.546,133.462L367.133,76.39L254.078,210.715L140.967,73.702l-61.513,65.068\n\n\tc33.791,43.885,78.146,89.797,123.688,132.465L82.993,413.987l19.865,22.629c29.251-20.31,87.839-65.578,150.312-120.092\n\n\tc63.662,55.812,122.861,101.336,151.301,121.773l21.438-19.443L303.804,270.95C352.439,225.709,399.308,177.442,432.546,133.462z\"/>\n\n</svg>\n\n"; |  | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -461,7 +437,7 @@ var colno = null; | |||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n    <title>Certidude server</title>\n    <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n    <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n    <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n    <nav id=\"menu\">\n        <ul class=\"container\">\n          <li data-section=\"requests\">Requests</li>\n          <li data-section=\"signed\">Signed</li>\n          <li data-section=\"revoked\">Revoked</li>\n          <li data-section=\"config\">Configuration</li>\n          <li data-section=\"log\">Log</li>\n        </ul>\n    </nav>\n    <div id=\"container\" class=\"container\">\n        Loading certificate authority...\n    </div>\n</body>\n\n<footer>\n    <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n    <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n"; | output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n    <title>Certidude server</title>\n    <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n    <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n    <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n    <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n    <nav id=\"menu\">\n        <ul class=\"container\">\n          <li data-section=\"about\">Profile</li>\n          <li id=\"section-requests\" data-section=\"requests\" style=\"display:none;\">Requests</li>\n          <li id=\"section-signed\" data-section=\"signed\" style=\"display:none;\">Signed</li>\n          <li id=\"section-revoked\" data-section=\"revoked\" style=\"display:none;\">Revoked</li>\n          <li id=\"section-config\" data-section=\"config\" style=\"display:none;\">Configuration</li>\n          <li id=\"section-log\" data-section=\"log\" style=\"display:none;\">Log</li>\n        </ul>\n    </nav>\n    <div id=\"container\" class=\"container\">\n        Loading certificate authority...\n    </div>\n</body>\n\n<footer>\n    <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n    <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| @@ -485,12 +461,38 @@ var colno = null; | |||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "\n<section id=\"about\">\n<p>Hi "; | output += "\n<section id=\"about\">\n<h2>"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"username"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"gn"), env.opts.autoescape); | ||||||
| output += ",</p>\n\n<p>Request submission is allowed from: "; | output += " "; | ||||||
| if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets")) { | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"sn"), env.opts.autoescape); | ||||||
|  | output += " ("; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"name"), env.opts.autoescape); | ||||||
|  | output += ") settings</h2>\n\n<p>Mails will be sent to: "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape); | ||||||
|  | output += "</p>\n\n<p>You can click <a href=\"/api/bundle/\">here</a> to generate bundle\nfor current user account.</p>\n\n"; | ||||||
|  | if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { | ||||||
|  | output += "\n\n<h2>Authority certificate</h2>\n\n<p>Several things such as CRL location and e-mails are hardcoded into\nthe <a href=\"/api/certificate\">certificate</a> and\nas such require complete reset of X509 infrastructure if some of them needs to be changed:</p>\n\n<p>Mails will appear from: "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"certificate")),"email_address"), env.opts.autoescape); | ||||||
|  | output += "</p>\n\n\n<h2>Authority settings</h2>\n\n<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>\n\n<p>Outgoing mail server:\n"; | ||||||
|  | if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox")) { | ||||||
|  | output += "\n    "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox"), env.opts.autoescape); | ||||||
|  | output += "\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | else { | ||||||
|  | output += "\n    E-mail disabled\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "</p>\n\n<p>Authenticated users allowed from:\n\n"; | ||||||
|  | if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) { | ||||||
|  | output += "\n    anywhere\n    </p>\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | else { | ||||||
|  | output += "\n    </p>\n    <ul>\n        "; | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); | var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"); | ||||||
| if(t_3) {var t_2 = t_3.length; | if(t_3) {var t_2 = t_3.length; | ||||||
| for(var t_1=0; t_1 < t_3.length; t_1++) { | for(var t_1=0; t_1 < t_3.length; t_1++) { | ||||||
| var t_4 = t_3[t_1]; | var t_4 = t_3[t_1]; | ||||||
| @@ -502,26 +504,29 @@ frame.set("loop.revindex0", t_2 - t_1 - 1); | |||||||
| frame.set("loop.first", t_1 === 0); | frame.set("loop.first", t_1 === 0); | ||||||
| frame.set("loop.last", t_1 === t_2 - 1); | frame.set("loop.last", t_1 === t_2 - 1); | ||||||
| frame.set("loop.length", t_2); | frame.set("loop.length", t_2); | ||||||
|  | output += "\n            <li>"; | ||||||
| output += runtime.suppressValue(t_4, env.opts.autoescape); | output += runtime.suppressValue(t_4, env.opts.autoescape); | ||||||
| output += " "; | output += "</li>\n        "; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| } | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
|  | output += "\n    </ul>\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "\n\n\n<p>Request submission is allowed from:\n\n"; | ||||||
|  | if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) { | ||||||
|  | output += "\n    anywhere\n    </p>\n"; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| else { | else { | ||||||
| output += "anywhere"; | output += "\n    </p>\n    <ul>\n        "; | ||||||
| ; |  | ||||||
| } |  | ||||||
| output += "</p>\n<p>Autosign is allowed from: "; |  | ||||||
| if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets")) { |  | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"); | var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); | ||||||
| if(t_7) {var t_6 = t_7.length; | if(t_7) {var t_6 = t_7.length; | ||||||
| for(var t_5=0; t_5 < t_7.length; t_5++) { | for(var t_5=0; t_5 < t_7.length; t_5++) { | ||||||
| var t_8 = t_7[t_5]; | var t_8 = t_7[t_5]; | ||||||
| frame.set("i", t_8); | frame.set("subnet", t_8); | ||||||
| frame.set("loop.index", t_5 + 1); | frame.set("loop.index", t_5 + 1); | ||||||
| frame.set("loop.index0", t_5); | frame.set("loop.index0", t_5); | ||||||
| frame.set("loop.revindex", t_6 - t_5); | frame.set("loop.revindex", t_6 - t_5); | ||||||
| @@ -529,26 +534,29 @@ frame.set("loop.revindex0", t_6 - t_5 - 1); | |||||||
| frame.set("loop.first", t_5 === 0); | frame.set("loop.first", t_5 === 0); | ||||||
| frame.set("loop.last", t_5 === t_6 - 1); | frame.set("loop.last", t_5 === t_6 - 1); | ||||||
| frame.set("loop.length", t_6); | frame.set("loop.length", t_6); | ||||||
|  | output += "\n            <li>"; | ||||||
| output += runtime.suppressValue(t_8, env.opts.autoescape); | output += runtime.suppressValue(t_8, env.opts.autoescape); | ||||||
| output += " "; | output += "</li>\n        "; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| } | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
|  | output += "\n    </ul>\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "\n\n<p>Autosign is allowed from:\n"; | ||||||
|  | if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) { | ||||||
|  | output += "\n    anywhere\n    </p>\n"; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| else { | else { | ||||||
| output += "nowhere"; | output += "\n    </p>\n    <ul>\n        "; | ||||||
| ; |  | ||||||
| } |  | ||||||
| output += "</p>\n<p>Authority administration is allowed from: "; |  | ||||||
| if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets")) { |  | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); | var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"); | ||||||
| if(t_11) {var t_10 = t_11.length; | if(t_11) {var t_10 = t_11.length; | ||||||
| for(var t_9=0; t_9 < t_11.length; t_9++) { | for(var t_9=0; t_9 < t_11.length; t_9++) { | ||||||
| var t_12 = t_11[t_9]; | var t_12 = t_11[t_9]; | ||||||
| frame.set("i", t_12); | frame.set("subnet", t_12); | ||||||
| frame.set("loop.index", t_9 + 1); | frame.set("loop.index", t_9 + 1); | ||||||
| frame.set("loop.index0", t_9); | frame.set("loop.index0", t_9); | ||||||
| frame.set("loop.revindex", t_10 - t_9); | frame.set("loop.revindex", t_10 - t_9); | ||||||
| @@ -556,25 +564,29 @@ frame.set("loop.revindex0", t_10 - t_9 - 1); | |||||||
| frame.set("loop.first", t_9 === 0); | frame.set("loop.first", t_9 === 0); | ||||||
| frame.set("loop.last", t_9 === t_10 - 1); | frame.set("loop.last", t_9 === t_10 - 1); | ||||||
| frame.set("loop.length", t_10); | frame.set("loop.length", t_10); | ||||||
|  | output += "\n            <li>"; | ||||||
| output += runtime.suppressValue(t_12, env.opts.autoescape); | output += runtime.suppressValue(t_12, env.opts.autoescape); | ||||||
| output += " "; | output += "</li>\n        "; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| } | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
|  | output += "\n    </ul>\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "\n\n<p>Authority administration is allowed from:\n"; | ||||||
|  | if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) { | ||||||
|  | output += "\n    anywhere\n    </p>\n"; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| else { | else { | ||||||
| output += "anywhere"; | output += "\n    <ul>\n        "; | ||||||
| ; |  | ||||||
| } |  | ||||||
| output += "\n<p>Authority administration allowed for: "; |  | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); | var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); | ||||||
| if(t_15) {var t_14 = t_15.length; | if(t_15) {var t_14 = t_15.length; | ||||||
| for(var t_13=0; t_13 < t_15.length; t_13++) { | for(var t_13=0; t_13 < t_15.length; t_13++) { | ||||||
| var t_16 = t_15[t_13]; | var t_16 = t_15[t_13]; | ||||||
| frame.set("i", t_16); | frame.set("subnet", t_16); | ||||||
| frame.set("loop.index", t_13 + 1); | frame.set("loop.index", t_13 + 1); | ||||||
| frame.set("loop.index0", t_13); | frame.set("loop.index0", t_13); | ||||||
| frame.set("loop.revindex", t_14 - t_13); | frame.set("loop.revindex", t_14 - t_13); | ||||||
| @@ -582,71 +594,132 @@ frame.set("loop.revindex0", t_14 - t_13 - 1); | |||||||
| frame.set("loop.first", t_13 === 0); | frame.set("loop.first", t_13 === 0); | ||||||
| frame.set("loop.last", t_13 === t_14 - 1); | frame.set("loop.last", t_13 === t_14 - 1); | ||||||
| frame.set("loop.length", t_14); | frame.set("loop.length", t_14); | ||||||
|  | output += "\n            <li>"; | ||||||
| output += runtime.suppressValue(t_16, env.opts.autoescape); | output += runtime.suppressValue(t_16, env.opts.autoescape); | ||||||
| output += " "; | output += "</li>\n        "; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| } | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
| output += "</p>\n</section>\n"; | output += "\n    </ul>\n"; | ||||||
| var t_17; | ; | ||||||
| t_17 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); |  | ||||||
| frame.set("s", t_17, true); |  | ||||||
| if(frame.topLevel) { |  | ||||||
| context.setVariable("s", t_17); |  | ||||||
| } | } | ||||||
| if(frame.topLevel) { | output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n"; | ||||||
| context.addExport("s", t_17); |  | ||||||
| } |  | ||||||
| output += "\n\n\n<section id=\"requests\">\n    <h1>Pending requests</h1>\n\n\n    <ul id=\"pending_requests\">\n        "; |  | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_20 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"requests"); | var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); | ||||||
| runtime.asyncEach(t_20, 1, function(request, t_18, t_19,next) { | if(t_19) {var t_17; | ||||||
| frame.set("request", request); | if(runtime.isArray(t_19)) { | ||||||
| frame.set("loop.index", t_18 + 1); | var t_18 = t_19.length; | ||||||
| frame.set("loop.index0", t_18); | for(t_17=0; t_17 < t_19.length; t_17++) { | ||||||
| frame.set("loop.revindex", t_19 - t_18); | var t_20 = t_19[t_17][0] | ||||||
| frame.set("loop.revindex0", t_19 - t_18 - 1); | frame.set("handle", t_19[t_17][0]); | ||||||
| frame.set("loop.first", t_18 === 0); | var t_21 = t_19[t_17][1] | ||||||
| frame.set("loop.last", t_18 === t_19 - 1); | frame.set("full_name", t_19[t_17][1]); | ||||||
| frame.set("loop.length", t_19); | frame.set("loop.index", t_17 + 1); | ||||||
| output += "\n             "; | frame.set("loop.index0", t_17); | ||||||
| env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_23,t_21) { | frame.set("loop.revindex", t_18 - t_17); | ||||||
| if(t_23) { cb(t_23); return; } | frame.set("loop.revindex0", t_18 - t_17 - 1); | ||||||
| t_21.render(context.getVariables(), frame, function(t_24,t_22) { | frame.set("loop.first", t_17 === 0); | ||||||
| if(t_24) { cb(t_24); return; } | frame.set("loop.last", t_17 === t_18 - 1); | ||||||
| output += t_22 | frame.set("loop.length", t_18); | ||||||
| output += "\n\t    "; | output += "\n    <li>"; | ||||||
| next(t_18); | output += runtime.suppressValue(t_21, env.opts.autoescape); | ||||||
| })}); | output += "</li>\n"; | ||||||
| }, function(t_26,t_25) { | ; | ||||||
| if(t_26) { cb(t_26); return; } | } | ||||||
|  | } else { | ||||||
|  | t_17 = -1; | ||||||
|  | var t_18 = runtime.keys(t_19).length; | ||||||
|  | for(var t_22 in t_19) { | ||||||
|  | t_17++; | ||||||
|  | var t_23 = t_19[t_22]; | ||||||
|  | frame.set("handle", t_22); | ||||||
|  | frame.set("full_name", t_23); | ||||||
|  | frame.set("loop.index", t_17 + 1); | ||||||
|  | frame.set("loop.index0", t_17); | ||||||
|  | frame.set("loop.revindex", t_18 - t_17); | ||||||
|  | frame.set("loop.revindex0", t_18 - t_17 - 1); | ||||||
|  | frame.set("loop.first", t_17 === 0); | ||||||
|  | frame.set("loop.last", t_17 === t_18 - 1); | ||||||
|  | frame.set("loop.length", t_18); | ||||||
|  | output += "\n    <li>"; | ||||||
|  | output += runtime.suppressValue(t_23, env.opts.autoescape); | ||||||
|  | output += "</li>\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | } | ||||||
|  | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
| output += "\n        <li class=\"notify\">\n            <p>No certificate signing requests to sign! You can  submit a certificate signing request by:</p>\n            <pre>certidude setup client "; | output += "\n</ul>\n</section>\n\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | else { | ||||||
|  | output += "\n<p>Here you can renew your certificates</p>\n\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "\n\n"; | ||||||
|  | var t_24; | ||||||
|  | t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); | ||||||
|  | frame.set("s", t_24, true); | ||||||
|  | if(frame.topLevel) { | ||||||
|  | context.setVariable("s", t_24); | ||||||
|  | } | ||||||
|  | if(frame.topLevel) { | ||||||
|  | context.addExport("s", t_24); | ||||||
|  | } | ||||||
|  | output += "\n\n\n"; | ||||||
|  | if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { | ||||||
|  | output += "\n<section id=\"requests\">\n    <h1>Pending requests</h1>\n\n    <p>Submit a certificate signing request with Certidude:</p>\n    <pre>certidude setup client "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape); | ||||||
| output += "</pre>\n        </li>\n    </ul>\n</section>\n\n\n<section id=\"signed\">\n    <h1>Signed certificates</h1>\n    <input id=\"search\" type=\"search\" class=\"icon search\">\n    <ul id=\"signed_certificates\">\n        "; | output += "</pre>\n\n    <ul id=\"pending_requests\">\n        "; | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_29 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"signed"))); | var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); | ||||||
| runtime.asyncEach(t_29, 1, function(certificate, t_27, t_28,next) { | if(t_27) {var t_26 = t_27.length; | ||||||
| frame.set("certificate", certificate); | for(var t_25=0; t_25 < t_27.length; t_25++) { | ||||||
| frame.set("loop.index", t_27 + 1); | var t_28 = t_27[t_25]; | ||||||
| frame.set("loop.index0", t_27); | frame.set("request", t_28); | ||||||
| frame.set("loop.revindex", t_28 - t_27); | frame.set("loop.index", t_25 + 1); | ||||||
| frame.set("loop.revindex0", t_28 - t_27 - 1); | frame.set("loop.index0", t_25); | ||||||
| frame.set("loop.first", t_27 === 0); | frame.set("loop.revindex", t_26 - t_25); | ||||||
| frame.set("loop.last", t_27 === t_28 - 1); | frame.set("loop.revindex0", t_26 - t_25 - 1); | ||||||
| frame.set("loop.length", t_28); | frame.set("loop.first", t_25 === 0); | ||||||
|  | frame.set("loop.last", t_25 === t_26 - 1); | ||||||
|  | frame.set("loop.length", t_26); | ||||||
| output += "\n             "; | 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; } | if(t_32) { cb(t_32); return; } | ||||||
| t_30.render(context.getVariables(), frame, function(t_33,t_31) { | output += t_30 | ||||||
| if(t_33) { cb(t_33); return; } |  | ||||||
| output += t_31 |  | ||||||
| output += "\n\t    "; | output += "\n\t    "; | ||||||
| next(t_27); |  | ||||||
| })}); | })}); | ||||||
| }, function(t_35,t_34) { | } | ||||||
| if(t_35) { cb(t_35); return; } | } | ||||||
|  | frame = frame.pop(); | ||||||
|  | output += "\n        <li class=\"notify\">\n            <p>No certificate signing requests to sign!</p>\n        </li>\n    </ul>\n</section>\n\n<section id=\"signed\">\n    <h1>Signed certificates</h1>\n    <input id=\"search\" type=\"search\" class=\"icon search\">\n    <ul id=\"signed_certificates\">\n        "; | ||||||
|  | frame = frame.push(); | ||||||
|  | var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); | ||||||
|  | if(t_35) {var t_34 = t_35.length; | ||||||
|  | for(var t_33=0; t_33 < t_35.length; t_33++) { | ||||||
|  | var t_36 = t_35[t_33]; | ||||||
|  | frame.set("certificate", t_36); | ||||||
|  | frame.set("loop.index", t_33 + 1); | ||||||
|  | frame.set("loop.index0", t_33); | ||||||
|  | frame.set("loop.revindex", t_34 - t_33); | ||||||
|  | frame.set("loop.revindex0", t_34 - t_33 - 1); | ||||||
|  | frame.set("loop.first", t_33 === 0); | ||||||
|  | frame.set("loop.last", t_33 === t_34 - 1); | ||||||
|  | frame.set("loop.length", t_34); | ||||||
|  | output += "\n            "; | ||||||
|  | env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) { | ||||||
|  | if(t_39) { cb(t_39); return; } | ||||||
|  | t_37.render(context.getVariables(), frame, function(t_40,t_38) { | ||||||
|  | if(t_40) { cb(t_40); return; } | ||||||
|  | output += t_38 | ||||||
|  | output += "\n\t    "; | ||||||
|  | })}); | ||||||
|  | } | ||||||
|  | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
| output += "\n    </ul>\n</section>\n\n<section id=\"log\">\n    <h1>Log</h1>\n    <p>\n        <input id=\"log_level_critical\" type=\"checkbox\" checked/> <label for=\"log_level_critical\">Critical</label>\n        <input id=\"log_level_error\" type=\"checkbox\" checked/> <label for=\"log_level_error\">Errors</label>\n        <input id=\"log_level_warning\" type=\"checkbox\" checked/> <label for=\"log_level_warning\">Warnings</label>\n        <input id=\"log_level_info\" type=\"checkbox\" checked/> <label for=\"log_level_info\">Info</label>\n        <input id=\"log_level_debug\" type=\"checkbox\"/> <label for=\"log_level_debug\">Debug</label>\n    </p>\n    <ul id=\"log_entries\">\n    </ul>\n</section>\n\n<section id=\"revoked\">\n    <h1>Revoked certificates</h1>\n    <p>To fetch certificate revocation list:</p>\n    <pre>curl "; | output += "\n    </ul>\n</section>\n\n<section id=\"log\">\n    <h1>Log</h1>\n    <p>\n        <input id=\"log_level_critical\" type=\"checkbox\" checked/> <label for=\"log_level_critical\">Critical</label>\n        <input id=\"log_level_error\" type=\"checkbox\" checked/> <label for=\"log_level_error\">Errors</label>\n        <input id=\"log_level_warning\" type=\"checkbox\" checked/> <label for=\"log_level_warning\">Warnings</label>\n        <input id=\"log_level_info\" type=\"checkbox\" checked/> <label for=\"log_level_info\">Info</label>\n        <input id=\"log_level_debug\" type=\"checkbox\"/> <label for=\"log_level_debug\">Debug</label>\n    </p>\n    <ul id=\"log_entries\">\n    </ul>\n</section>\n\n<section id=\"revoked\">\n    <h1>Revoked certificates</h1>\n    <p>To fetch certificate revocation list:</p>\n    <pre>curl "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape); | ||||||
| @@ -656,41 +729,44 @@ output += "/certificate/ > session.pem\n    openssl ocsp -issuer session.pem -CA | |||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape); | ||||||
| output += "/ocsp/ -serial 0x\n    </pre>\n    -->\n    <ul>\n        "; | output += "/ocsp/ -serial 0x\n    </pre>\n    -->\n    <ul>\n        "; | ||||||
| frame = frame.push(); | frame = frame.push(); | ||||||
| var t_38 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"revoked"); | var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); | ||||||
| if(t_38) {var t_37 = t_38.length; | if(t_43) {var t_42 = t_43.length; | ||||||
| for(var t_36=0; t_36 < t_38.length; t_36++) { | for(var t_41=0; t_41 < t_43.length; t_41++) { | ||||||
| var t_39 = t_38[t_36]; | var t_44 = t_43[t_41]; | ||||||
| frame.set("j", t_39); | frame.set("j", t_44); | ||||||
| frame.set("loop.index", t_36 + 1); | frame.set("loop.index", t_41 + 1); | ||||||
| frame.set("loop.index0", t_36); | frame.set("loop.index0", t_41); | ||||||
| frame.set("loop.revindex", t_37 - t_36); | frame.set("loop.revindex", t_42 - t_41); | ||||||
| frame.set("loop.revindex0", t_37 - t_36 - 1); | frame.set("loop.revindex0", t_42 - t_41 - 1); | ||||||
| frame.set("loop.first", t_36 === 0); | frame.set("loop.first", t_41 === 0); | ||||||
| frame.set("loop.last", t_36 === t_37 - 1); | frame.set("loop.last", t_41 === t_42 - 1); | ||||||
| frame.set("loop.length", t_37); | frame.set("loop.length", t_42); | ||||||
| output += "\n            <li id=\"certificate_"; | output += "\n            <li id=\"certificate_"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((t_39),"sha256sum"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape); | ||||||
| output += "\">\n                "; | output += "\">\n                "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((t_39),"changed"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape); | ||||||
| output += "\n                "; | output += "\n                "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((t_39),"serial_number"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape); | ||||||
| output += " <span class=\"monospace\">"; | output += " <span class=\"monospace\">"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((t_39),"identity"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape); | ||||||
| output += "</span>\n            </li>\n        "; | output += "</span>\n            </li>\n        "; | ||||||
| ; | ; | ||||||
| } | } | ||||||
| } | } | ||||||
| if (!t_37) { | if (!t_42) { | ||||||
| output += "\n            <li>Great job! No certificate signing requests to sign.</li>\n\t    "; | output += "\n            <li>Great job! No certificate signing requests to sign.</li>\n\t    "; | ||||||
| } | } | ||||||
| frame = frame.pop(); | frame = frame.pop(); | ||||||
| output += "\n    </ul>\n</section>\n\n<section id=\"config\">\n</section>\n"; | output += "\n    </ul>\n</section>\n\n<section id=\"config\">\n</section>\n\n"; | ||||||
|  | ; | ||||||
|  | } | ||||||
|  | output += "\n"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| cb(null, output); | cb(null, output); | ||||||
| } | } | ||||||
| })}); | ; | ||||||
| } catch (e) { | } catch (e) { | ||||||
|   cb(runtime.handleError(e, lineno, colno)); |   cb(runtime.handleError(e, lineno, colno)); | ||||||
| } | } | ||||||
| @@ -894,8 +970,8 @@ var colno = null; | |||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<li id=\"request_"; | output += "<li id=\"request-"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); | output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"),"@","--"),".","-"), env.opts.autoescape); | ||||||
| output += "\" class=\"filterable\">\n\n<a class=\"button icon download\" href=\"/api/request/"; | output += "\" class=\"filterable\">\n\n<a class=\"button icon download\" href=\"/api/request/"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); | ||||||
| output += "/\">Fetch</a>\n"; | output += "/\">Fetch</a>\n"; | ||||||
| @@ -912,7 +988,7 @@ output += "\n<button title=\"Please use certidude command-line utility to sign u | |||||||
| output += "\n<button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/request/"; | output += "\n<button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/request/"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape); | ||||||
| output += "/',type:'delete'});\">Delete</button>\n\n\n<div class=\"monospace\">\n"; | output += "/',type:'delete'});\">Delete</button>\n\n\n<div class=\"monospace\">\n"; | ||||||
| env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/request.html", null, function(t_3,t_1) { | env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/request.html", null, function(t_3,t_1) { | ||||||
| if(t_3) { cb(t_3); return; } | if(t_3) { cb(t_3); return; } | ||||||
| t_1.render(context.getVariables(), frame, function(t_4,t_2) { | t_1.render(context.getVariables(), frame, function(t_4,t_2) { | ||||||
| if(t_4) { cb(t_4); return; } | if(t_4) { cb(t_4); return; } | ||||||
| @@ -920,9 +996,9 @@ output += t_2 | |||||||
| output += "\n"; | output += "\n"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape); | ||||||
| output += "\n</div>\n\n"; | output += "\n</div>\n\n"; | ||||||
| (function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) { | if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) { | ||||||
| output += "\n<div class=\"email\">"; | output += "\n<div class=\"email\">"; | ||||||
| env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/request.html", null, function(t_7,t_5) { | env.getTemplate("img/iconmonstr-email-2.svg", false, "views/request.html", null, function(t_7,t_5) { | ||||||
| if(t_7) { cb(t_7); return; } | if(t_7) { cb(t_7); return; } | ||||||
| t_5.render(context.getVariables(), frame, function(t_8,t_6) { | t_5.render(context.getVariables(), frame, function(t_8,t_6) { | ||||||
| if(t_8) { cb(t_8); return; } | if(t_8) { cb(t_8); return; } | ||||||
| @@ -930,12 +1006,10 @@ output += t_6 | |||||||
| output += " "; | output += " "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape); | ||||||
| output += "</div>\n"; | output += "</div>\n"; | ||||||
| cb()})}); | })}); | ||||||
| } | } | ||||||
| else { | output += "\n\n<div class=\"monospace\">\n"; | ||||||
| cb()} | env.getTemplate("img/iconmonstr-key-3.svg", false, "views/request.html", null, function(t_11,t_9) { | ||||||
| })(function() {output += "\n\n<div class=\"monospace\">\n"; |  | ||||||
| env.getTemplate("img/iconmonstr-key-2-icon.svg", false, "views/request.html", null, function(t_11,t_9) { |  | ||||||
| if(t_11) { cb(t_11); return; } | if(t_11) { cb(t_11); return; } | ||||||
| t_9.render(context.getVariables(), frame, function(t_12,t_10) { | t_9.render(context.getVariables(), frame, function(t_12,t_10) { | ||||||
| if(t_12) { cb(t_12); return; } | if(t_12) { cb(t_12); return; } | ||||||
| @@ -957,9 +1031,9 @@ if(frame.topLevel) { | |||||||
| context.addExport("key_usage", t_13); | context.addExport("key_usage", t_13); | ||||||
| } | } | ||||||
| output += "\n"; | output += "\n"; | ||||||
| (function(cb) {if(runtime.contextOrFrameLookup(context, frame, "key_usage")) { | if(runtime.contextOrFrameLookup(context, frame, "key_usage")) { | ||||||
| output += "\n<div>\n"; | output += "\n<div>\n"; | ||||||
| env.getTemplate("img/iconmonstr-flag-3-icon.svg", false, "views/request.html", null, function(t_16,t_14) { | env.getTemplate("img/iconmonstr-flag-3.svg", false, "views/request.html", null, function(t_16,t_14) { | ||||||
| if(t_16) { cb(t_16); return; } | if(t_16) { cb(t_16); return; } | ||||||
| t_14.render(context.getVariables(), frame, function(t_17,t_15) { | t_14.render(context.getVariables(), frame, function(t_17,t_15) { | ||||||
| if(t_17) { cb(t_17); return; } | if(t_17) { cb(t_17); return; } | ||||||
| @@ -967,17 +1041,15 @@ output += t_15 | |||||||
| output += "\n"; | output += "\n"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape); | ||||||
| output += "\n</div>\n"; | output += "\n</div>\n"; | ||||||
| cb()})}); | })}); | ||||||
| } | } | ||||||
| else { | output += "\n\n</li>\n\n"; | ||||||
| cb()} |  | ||||||
| })(function() {output += "\n\n</li>\n\n"; |  | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| cb(null, output); | cb(null, output); | ||||||
| } | } | ||||||
| })})})})})}); | })})})}); | ||||||
| } catch (e) { | } catch (e) { | ||||||
|   cb(runtime.handleError(e, lineno, colno)); |   cb(runtime.handleError(e, lineno, colno)); | ||||||
| } | } | ||||||
| @@ -995,8 +1067,8 @@ var colno = null; | |||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<li id=\"certificate_"; | output += "<li id=\"certificate-"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"sha256sum"), env.opts.autoescape); | output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"),"@","--"),".","-"), env.opts.autoescape); | ||||||
| output += "\" data-dn=\""; | output += "\" data-dn=\""; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); | ||||||
| output += "\" data-cn=\""; | output += "\" data-cn=\""; | ||||||
| @@ -1006,17 +1078,17 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook | |||||||
| output += "/\">Fetch</a>\n    <button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/"; | output += "/\">Fetch</a>\n    <button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/"; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); | ||||||
| output += "/',type:'delete'});\">Revoke</button>\n\n    <div class=\"monospace\">\n    "; | output += "/',type:'delete'});\">Revoke</button>\n\n    <div class=\"monospace\">\n    "; | ||||||
| env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/signed.html", null, function(t_3,t_1) { | env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/signed.html", null, function(t_3,t_1) { | ||||||
| if(t_3) { cb(t_3); return; } | if(t_3) { cb(t_3); return; } | ||||||
| t_1.render(context.getVariables(), frame, function(t_4,t_2) { | t_1.render(context.getVariables(), frame, function(t_4,t_2) { | ||||||
| if(t_4) { cb(t_4); return; } | if(t_4) { cb(t_4); return; } | ||||||
| output += t_2 | output += t_2 | ||||||
| output += "\n    "; | output += "\n    "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); | ||||||
| output += "\n    </div>\n\n    "; | output += "\n    </div>\n\n    "; | ||||||
| (function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) { | if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) { | ||||||
| output += "\n    <div class=\"email\">"; | output += "\n    <div class=\"email\">"; | ||||||
| env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/signed.html", null, function(t_7,t_5) { | env.getTemplate("img/iconmonstr-email-2.svg", false, "views/signed.html", null, function(t_7,t_5) { | ||||||
| if(t_7) { cb(t_7); return; } | if(t_7) { cb(t_7); return; } | ||||||
| t_5.render(context.getVariables(), frame, function(t_8,t_6) { | t_5.render(context.getVariables(), frame, function(t_8,t_6) { | ||||||
| if(t_8) { cb(t_8); return; } | if(t_8) { cb(t_8); return; } | ||||||
| @@ -1024,32 +1096,53 @@ output += t_6 | |||||||
| output += " "; | output += " "; | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape); | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape); | ||||||
| output += "</div>\n    "; | output += "</div>\n    "; | ||||||
| cb()})}); | })}); | ||||||
| } | } | ||||||
| else { | output += "\n    \n    "; | ||||||
| cb()} | if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) { | ||||||
| })(function() {output += "\n\n    "; | output += "\n    <div class=\"person\">"; | ||||||
| output += "\n\n    <div class=\"tags\">\n      <select class=\"icon tag\" data-cn=\""; | env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) { | ||||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); |  | ||||||
| output += "\" onChange=\"onNewTagClicked();\">\n        <option value=\"\">Add tag...</option>\n        "; |  | ||||||
| env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_11,t_9) { |  | ||||||
| if(t_11) { cb(t_11); return; } | if(t_11) { cb(t_11); return; } | ||||||
| t_9.render(context.getVariables(), frame, function(t_12,t_10) { | t_9.render(context.getVariables(), frame, function(t_12,t_10) { | ||||||
| if(t_12) { cb(t_12); return; } | if(t_12) { cb(t_12); return; } | ||||||
| output += t_10 | output += t_10 | ||||||
| output += "\n      </select>\n    </div>\n\n    <div class=\"status\">\n    "; | output += " "; | ||||||
| env.getTemplate("views/status.html", false, "views/signed.html", null, function(t_15,t_13) { | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name"), env.opts.autoescape); | ||||||
|  | output += " "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname"), env.opts.autoescape); | ||||||
|  | output += "</div>\n    "; | ||||||
|  | })}); | ||||||
|  | } | ||||||
|  | output += "\n\n    <div class=\"lifetime\" title=\"Valid from "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape); | ||||||
|  | output += " to "; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape); | ||||||
|  | output += "\">\n        "; | ||||||
|  | env.getTemplate("img/iconmonstr-calendar-6.svg", false, "views/signed.html", null, function(t_15,t_13) { | ||||||
| if(t_15) { cb(t_15); return; } | if(t_15) { cb(t_15); return; } | ||||||
| t_13.render(context.getVariables(), frame, function(t_16,t_14) { | t_13.render(context.getVariables(), frame, function(t_16,t_14) { | ||||||
| if(t_16) { cb(t_16); return; } | if(t_16) { cb(t_16); return; } | ||||||
| output += t_14 | output += t_14 | ||||||
| output += "\n    </div>\n</li>\n"; | output += "\n        <time>"; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape); | ||||||
|  | output += "</time> -\n        <time>"; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape); | ||||||
|  | output += "</time>\n    </div>\n\n    "; | ||||||
|  | output += "\n\n    <div class=\"tags\">\n        <select class=\"icon tag\" data-cn=\""; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); | ||||||
|  | output += "\" onChange=\"onNewTagClicked();\">\n        <option value=\"\">Add tag...</option>\n            "; | ||||||
|  | env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_19,t_17) { | ||||||
|  | if(t_19) { cb(t_19); return; } | ||||||
|  | t_17.render(context.getVariables(), frame, function(t_20,t_18) { | ||||||
|  | if(t_20) { cb(t_20); return; } | ||||||
|  | output += t_18 | ||||||
|  | output += "\n        </select>\n    </div>\n    <div class=\"status\"></div>\n</li>\n"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
| cb(null, output); | cb(null, output); | ||||||
| } | } | ||||||
| })})})})})})}); | })})})})})}); | ||||||
| } catch (e) { | } catch (e) { | ||||||
|   cb(runtime.handleError(e, lineno, colno)); |   cb(runtime.handleError(e, lineno, colno)); | ||||||
| } | } | ||||||
| @@ -1135,6 +1228,44 @@ return { | |||||||
| root: root | root: root | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | })(); | ||||||
|  | })(); | ||||||
|  | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tags.html"] = (function() { | ||||||
|  | function root(env, context, frame, runtime, cb) { | ||||||
|  | var lineno = null; | ||||||
|  | var colno = null; | ||||||
|  | var output = ""; | ||||||
|  | try { | ||||||
|  | var parentTemplate = null; | ||||||
|  | output += "<span id=\"tag_"; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape); | ||||||
|  | output += "\" onclick=\"onTagClicked()\"\ntitle=\""; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape); | ||||||
|  | output += "="; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape); | ||||||
|  | output += "\" class=\""; | ||||||
|  | output += runtime.suppressValue(env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"),"."," "), env.opts.autoescape); | ||||||
|  | output += "\"\ndata-id=\""; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape); | ||||||
|  | output += "\" data-key=\""; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape); | ||||||
|  | output += "\">"; | ||||||
|  | output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape); | ||||||
|  | output += "</span>\n"; | ||||||
|  | if(parentTemplate) { | ||||||
|  | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
|  | } else { | ||||||
|  | cb(null, output); | ||||||
|  | } | ||||||
|  | ; | ||||||
|  | } catch (e) { | ||||||
|  |   cb(runtime.handleError(e, lineno, colno)); | ||||||
|  | } | ||||||
|  | } | ||||||
|  | return { | ||||||
|  | root: root | ||||||
|  | }; | ||||||
|  |  | ||||||
| })(); | })(); | ||||||
| })(); | })(); | ||||||
| (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() { | (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() { | ||||||
| @@ -1144,7 +1275,7 @@ var colno = null; | |||||||
| var output = ""; | var output = ""; | ||||||
| try { | try { | ||||||
| var parentTemplate = null; | var parentTemplate = null; | ||||||
| output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channela\">5GHz channel number</option>\n<option value=\"wireless.channelb\">2.4GHz channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n"; | output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channel\">Channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n"; | ||||||
| if(parentTemplate) { | if(parentTemplate) { | ||||||
| parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); | ||||||
| } else { | } else { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								certidude/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | User-agent: * | ||||||
|  | Disallow: / | ||||||
| @@ -1,36 +1,127 @@ | |||||||
|  |  | ||||||
| <section id="about"> | <section id="about"> | ||||||
| <p>Hi {{session.username}},</p> | <h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2> | ||||||
|  |  | ||||||
| <p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p> | <p>Mails will be sent to: {{ session.user.mail }}</p> | ||||||
| <p>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p> |  | ||||||
| <p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %} | <p>You can click <a href="/api/bundle/">here</a> to generate bundle | ||||||
| <p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p> | for current user account.</p> | ||||||
| </section> |  | ||||||
| {% set s = session.certificate.identity %} | {% if session.authority %} | ||||||
|  |  | ||||||
|  | <h2>Authority certificate</h2> | ||||||
|  |  | ||||||
|  | <p>Several things such as CRL location and e-mails are hardcoded into | ||||||
|  | the <a href="/api/certificate">certificate</a> and | ||||||
|  | as such require complete reset of X509 infrastructure if some of them needs to be changed:</p> | ||||||
|  |  | ||||||
|  | <p>Mails will appear from: {{ session.authority.certificate.email_address }}</p> | ||||||
|  |  | ||||||
|  |  | ||||||
| <section id="requests"> | <h2>Authority settings</h2> | ||||||
|     <h1>Pending requests</h1> |  | ||||||
|  |  | ||||||
|  | <p>These can be reconfigured via /etc/certidude/server.conf on the server.</p> | ||||||
|  |  | ||||||
|     <ul id="pending_requests"> | <p>Outgoing mail server: | ||||||
|         {% for request in session.requests %} | {% if session.authority.outbox %} | ||||||
|              {% include "views/request.html" %} |     {{ 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 %} | {% endfor %} | ||||||
|         <li class="notify"> |  | ||||||
|             <p>No certificate signing requests to sign! You can  submit a certificate signing request by:</p> |  | ||||||
|             <pre>certidude setup client {{session.common_name}}</pre> |  | ||||||
|         </li> |  | ||||||
| </ul> | </ul> | ||||||
| </section> | </section> | ||||||
|  |  | ||||||
|  | {% else %} | ||||||
|  | <p>Here you can renew your certificates</p> | ||||||
|  |  | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
|  | {% set s = session.certificate.identity %} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | {% if session.authority %} | ||||||
|  | <section id="requests"> | ||||||
|  |     <h1>Pending requests</h1> | ||||||
|  |  | ||||||
|  |     <p>Submit a certificate signing request with Certidude:</p> | ||||||
|  |     <pre>certidude setup client {{session.common_name}}</pre> | ||||||
|  |  | ||||||
|  |     <ul id="pending_requests"> | ||||||
|  |         {% for request in session.authority.requests %} | ||||||
|  |              {% include "views/request.html" %} | ||||||
|  | 	    {% endfor %} | ||||||
|  |         <li class="notify"> | ||||||
|  |             <p>No certificate signing requests to sign!</p> | ||||||
|  |         </li> | ||||||
|  |     </ul> | ||||||
|  | </section> | ||||||
|  |  | ||||||
| <section id="signed"> | <section id="signed"> | ||||||
|     <h1>Signed certificates</h1> |     <h1>Signed certificates</h1> | ||||||
|     <input id="search" type="search" class="icon search"> |     <input id="search" type="search" class="icon search"> | ||||||
|     <ul id="signed_certificates"> |     <ul id="signed_certificates"> | ||||||
|         {% for certificate in session.signed | sort | reverse %} |         {% for certificate in session.authority.signed | sort | reverse %} | ||||||
|             {% include "views/signed.html" %} |             {% include "views/signed.html" %} | ||||||
| 	    {% endfor %} | 	    {% endfor %} | ||||||
|     </ul> |     </ul> | ||||||
| @@ -62,7 +153,7 @@ | |||||||
|     </pre> |     </pre> | ||||||
|     --> |     --> | ||||||
|     <ul> |     <ul> | ||||||
|         {% for j in session.revoked %} |         {% for j in session.authority.revoked %} | ||||||
|             <li id="certificate_{{ j.sha256sum }}"> |             <li id="certificate_{{ j.sha256sum }}"> | ||||||
|                 {{j.changed}} |                 {{j.changed}} | ||||||
|                 {{j.serial_number}} <span class="monospace">{{j.identity}}</span> |                 {{j.serial_number}} <span class="monospace">{{j.identity}}</span> | ||||||
| @@ -75,3 +166,5 @@ | |||||||
|  |  | ||||||
| <section id="config"> | <section id="config"> | ||||||
| </section> | </section> | ||||||
|  |  | ||||||
|  | {% endif %} | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <li id="request_{{ request.common_name }}" class="filterable"> | <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable"> | ||||||
|  |  | ||||||
| <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> | <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> | ||||||
| {% if request.signable %} | {% if request.signable %} | ||||||
| @@ -10,16 +10,16 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
| <div class="monospace"> | <div class="monospace"> | ||||||
| {% include 'img/iconmonstr-certificate-15-icon.svg' %} | {% include 'img/iconmonstr-certificate-15.svg' %} | ||||||
| {{request.identity}} | {{request.identity}} | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| {% if request.email_address %} | {% if request.email_address %} | ||||||
| <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ request.email_address }}</div> | <div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ request.email_address }}</div> | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
| <div class="monospace"> | <div class="monospace"> | ||||||
| {% include 'img/iconmonstr-key-2-icon.svg' %} | {% include 'img/iconmonstr-key-3.svg' %} | ||||||
| <span title="SHA-1 of public key"> | <span title="SHA-1 of public key"> | ||||||
| {{ request.sha256sum }} | {{ request.sha256sum }} | ||||||
| </span> | </span> | ||||||
| @@ -30,7 +30,7 @@ | |||||||
| {% set key_usage = request.key_usage %} | {% set key_usage = request.key_usage %} | ||||||
| {% if key_usage %} | {% if key_usage %} | ||||||
| <div> | <div> | ||||||
| {% include 'img/iconmonstr-flag-3-icon.svg' %} | {% include 'img/iconmonstr-flag-3.svg' %} | ||||||
| {{request.key_usage}} | {{request.key_usage}} | ||||||
| </div> | </div> | ||||||
| {% endif %} | {% endif %} | ||||||
|   | |||||||
| @@ -1,20 +1,30 @@ | |||||||
| <li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable"> | <li id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable"> | ||||||
|     <a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a> |     <a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a> | ||||||
|     <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button> |     <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button> | ||||||
|  |  | ||||||
|     <div class="monospace"> |     <div class="monospace"> | ||||||
|     {% include 'img/iconmonstr-certificate-15-icon.svg' %} |     {% include 'img/iconmonstr-certificate-15.svg' %} | ||||||
|     {{certificate.identity}} |     {{certificate.common_name}} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     {% if certificate.email_address %} |     {% if certificate.email_address %} | ||||||
|     <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ certificate.email_address }}</div> |     <div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ certificate.email_address }}</div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  |     {% if certificate.given_name or certificate.surname %} | ||||||
|  |     <div class="person">{% include 'img/iconmonstr-user-5.svg' %} {{ certificate.given_name }} {{ certificate.surname }}</div> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     <div class="lifetime" title="Valid from {{ certificate.signed }} to {{ certificate.expires }}"> | ||||||
|  |         {% include 'img/iconmonstr-calendar-6.svg' %} | ||||||
|  |         <time>{{ certificate.signed }}</time> - | ||||||
|  |         <time>{{ certificate.expires }}</time> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|     {# |     {# | ||||||
|  |  | ||||||
|     <div class="monospace"> |     <div class="monospace"> | ||||||
|     {% include 'img/iconmonstr-key-2-icon.svg' %} |     {% include 'img/iconmonstr-key-3.svg' %} | ||||||
|     <span title="SHA-256 of public key"> |     <span title="SHA-256 of public key"> | ||||||
|     {{ certificate.sha256sum }} |     {{ certificate.sha256sum }} | ||||||
|     </span> |     </span> | ||||||
| @@ -23,7 +33,7 @@ | |||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div> |     <div> | ||||||
|     {% include 'img/iconmonstr-flag-3-icon.svg' %} |     {% include 'img/iconmonstr-flag-3.svg' %} | ||||||
|     {{certificate.key_usage}} |     {{certificate.key_usage}} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| @@ -35,8 +45,5 @@ | |||||||
|             {% include 'views/tagtypes.html' %} |             {% include 'views/tagtypes.html' %} | ||||||
|         </select> |         </select> | ||||||
|     </div> |     </div> | ||||||
|  |     <div class="status"></div> | ||||||
|     <div class="status"> |  | ||||||
|     {% include 'views/status.html' %} |  | ||||||
|     </div> |  | ||||||
| </li> | </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.password">Protected wireless network password</option> | ||||||
| <option value="wireless.protected.name">Protected wireless network name</option> | <option value="wireless.protected.name">Protected wireless network name</option> | ||||||
| <option value="wireless.public.name">Public wireless network name</option> | <option value="wireless.public.name">Public wireless network name</option> | ||||||
| <option value="wireless.channela">5GHz channel number</option> | <option value="wireless.channel">Channel number</option> | ||||||
| <option value="wireless.channelb">2.4GHz channel number</option> |  | ||||||
| <option value="usb.approved">Approved USB device</option> | <option value="usb.approved">Approved USB device</option> | ||||||
|   | |||||||
| @@ -1,20 +1,60 @@ | |||||||
|  | [authentication] | ||||||
|  | backends = pam | ||||||
|  | #backends = kerberos | ||||||
|  | #backends = ldap | ||||||
|  | #backends = kerberos ldap | ||||||
|  | #backends = kerberos pam | ||||||
|  |  | ||||||
|  | [accounts] | ||||||
|  | backend = posix | ||||||
|  | #backend = ldap | ||||||
|  |  | ||||||
| [authorization] | [authorization] | ||||||
| admin_users = administrator | backend = posix | ||||||
| admin_subnets = 0.0.0.0/0 | #backend = ldap | ||||||
| request_subnets = 0.0.0.0/0 | whitelist admin users = root administrator | ||||||
| autosign_subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 | ldap gssapi credential cache = /run/certidude/krb5cc | ||||||
|  |  | ||||||
|  | ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) | ||||||
|  | ldap user filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s)) | ||||||
|  | ldap admins filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=koodur,dc=com)(samaccountname=%s)) | ||||||
|  | ldap member of filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s)(memberOf=%s)) | ||||||
|  | ldap members filter = (&(objectclass=group)(cn=%s)(member=%s)) | ||||||
|  |  | ||||||
|  | ldap group filter = (&(objectClass=group)(cn=%s)(member=%s)) | ||||||
|  | ldap user group = | ||||||
|  | ldap admin group = domain admins | ||||||
|  | posix user group = | ||||||
|  | posix admin group = certidude | ||||||
|  | user subnets = 0.0.0.0/0 | ||||||
|  | admin subnets = 0.0.0.0/0 | ||||||
|  | request subnets = 0.0.0.0/0 | ||||||
|  | autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 | ||||||
|  |  | ||||||
|  | [logging] | ||||||
|  | backend = sql | ||||||
|  | database = sqlite://{{ directory }}/db.sqlite | ||||||
|  |  | ||||||
|  | [tagging] | ||||||
|  | backend = sql | ||||||
|  | database = sqlite://{{ directory }}/db.sqlite | ||||||
|  |  | ||||||
|  | [leases] | ||||||
|  | backend = sql | ||||||
|  | schema = strongswan | ||||||
|  | database = sqlite://{{ directory }}/db.sqlite | ||||||
|  |  | ||||||
| [signature] | [signature] | ||||||
| certificate_lifetime = 1825 | certificate lifetime = 1825 | ||||||
| revocation_list_lifetime = 1 | revocation list lifetime = 1 | ||||||
|  |  | ||||||
| [push] | [push] | ||||||
| server = | server = | ||||||
|  |  | ||||||
| [authority] | [authority] | ||||||
| private_key_path = {{ ca_key }} | private key path = {{ ca_key }} | ||||||
| certificate_path = {{ ca_crt }} | certificate path = {{ ca_crt }} | ||||||
| requests_dir = {{ directory }}/requests/ | requests dir = {{ directory }}/requests/ | ||||||
| signed_dir = {{ directory }}/signed/ | signed dir = {{ directory }}/signed/ | ||||||
| revoked_dir = {{ directory }}/revoked/ | revoked dir = {{ directory }}/revoked/ | ||||||
|  | outbox = smtp://localhost | ||||||
|   | |||||||
							
								
								
									
										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 %} |         {% if not push_server %} | ||||||
|         location ~ /publish/(.*) { |         location /pub { | ||||||
|             allow 127.0.0.1; |             allow 127.0.0.1; | ||||||
|             push_stream_publisher admin; |             nchan_publisher http; | ||||||
|             push_stream_channels_path $1; |             nchan_store_messages off; | ||||||
|  |             nchan_channel_id $arg_id; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         location ~ /subscribe/(.*) { |         location ~ "^/lp/(.*)" { | ||||||
|             push_stream_channels_path $1; |             nchan_subscriber longpoll; | ||||||
|             push_stream_subscriber long-polling; |             nchan_channel_id $1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         location ~ "^/ev/(.*)" { | ||||||
|  |             nchan_subscriber eventsource; | ||||||
|  |             nchan_channel_id $1; | ||||||
|         } |         } | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| [uwsgi] | [uwsgi] | ||||||
| exec-as-root = /usr/local/bin/certidude spawn | exec-as-root = /usr/local/bin/certidude signer spawn -k | ||||||
| master = true | master = true | ||||||
| processes = 1 | processes = 1 | ||||||
| vacuum = true | vacuum = true | ||||||
| @@ -15,3 +15,5 @@ buffer-size = 32768 | |||||||
| env = LANG=C.UTF-8 | env = LANG=C.UTF-8 | ||||||
| env = LC_ALL=C.UTF-8 | env = LC_ALL=C.UTF-8 | ||||||
| env = KRB5_KTNAME={{kerberos_keytab}} | env = KRB5_KTNAME={{kerberos_keytab}} | ||||||
|  | env = KRB5CCNAME=/run/certidude/krb5cc | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,10 +3,9 @@ import hashlib | |||||||
| import re | import re | ||||||
| import click | import click | ||||||
| import io | import io | ||||||
| from Crypto.Util import asn1 | from certidude import constants | ||||||
| from OpenSSL import crypto | from OpenSSL import crypto | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from certidude.signer import raw_sign, EXTENSION_WHITELIST |  | ||||||
|  |  | ||||||
| def subject2dn(subject): | def subject2dn(subject): | ||||||
|     bits = [] |     bits = [] | ||||||
| @@ -16,6 +15,10 @@ def subject2dn(subject): | |||||||
|     return ", ".join(bits) |     return ", ".join(bits) | ||||||
|  |  | ||||||
| class CertificateBase: | class CertificateBase: | ||||||
|  |     # Others will cause browsers to import the cert instead of offering to | ||||||
|  |     # download it | ||||||
|  |     content_type = "application/x-pem-file" | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return self.buf |         return self.buf | ||||||
|  |  | ||||||
| @@ -41,7 +44,7 @@ class CertificateBase: | |||||||
|  |  | ||||||
|     @common_name.setter |     @common_name.setter | ||||||
|     def common_name(self, value): |     def common_name(self, value): | ||||||
|         return setattr(self._obj.get_subject(), "CN", value) |         self.subject.CN = value | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def country_code(self): |     def country_code(self): | ||||||
| @@ -130,7 +133,6 @@ class CertificateBase: | |||||||
|     def set_extensions(self, extensions): |     def set_extensions(self, extensions): | ||||||
|         # X509Req().add_extensions() first invocation takes only effect?! |         # X509Req().add_extensions() first invocation takes only effect?! | ||||||
|         assert self._obj.get_extensions() == [], "Extensions already set!" |         assert self._obj.get_extensions() == [], "Extensions already set!" | ||||||
|  |  | ||||||
|         self._obj.add_extensions([ |         self._obj.add_extensions([ | ||||||
|             crypto.X509Extension( |             crypto.X509Extension( | ||||||
|                 key.encode("ascii"), |                 key.encode("ascii"), | ||||||
| @@ -164,6 +166,7 @@ class CertificateBase: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pubkey(self): |     def pubkey(self): | ||||||
|  |         from Crypto.Util import asn1 | ||||||
|         pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey()) |         pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey()) | ||||||
|         pubkey_der=asn1.DerSequence() |         pubkey_der=asn1.DerSequence() | ||||||
|         pubkey_der.decode(pubkey_asn1) |         pubkey_der.decode(pubkey_asn1) | ||||||
| @@ -194,6 +197,11 @@ class CertificateBase: | |||||||
|  |  | ||||||
|  |  | ||||||
| class Request(CertificateBase): | class Request(CertificateBase): | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def suggested_filename(self): | ||||||
|  |         return self.common_name + ".csr" | ||||||
|  |  | ||||||
|     def __init__(self, mixed=None): |     def __init__(self, mixed=None): | ||||||
|         self.buf = None |         self.buf = None | ||||||
|         self.path = NotImplemented |         self.path = NotImplemented | ||||||
| @@ -204,27 +212,23 @@ class Request(CertificateBase): | |||||||
|             _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) |             _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) | ||||||
|             self.created = datetime.fromtimestamp(mtime) |             self.created = datetime.fromtimestamp(mtime) | ||||||
|             mixed = mixed.read() |             mixed = mixed.read() | ||||||
|         if isinstance(mixed, bytes): |  | ||||||
|             mixed = mixed.decode("ascii") |  | ||||||
|         if isinstance(mixed, str): |         if isinstance(mixed, str): | ||||||
|             try: |             try: | ||||||
|                 self.buf = mixed |                 self.buf = mixed | ||||||
|                 mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed) |                 mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed) | ||||||
|             except crypto.Error: |             except crypto.Error: | ||||||
|                 print("Failed to parse:", mixed) |                 raise ValueError("Failed to parse: %s" % mixed) | ||||||
|                 raise |  | ||||||
|  |  | ||||||
|         if isinstance(mixed, crypto.X509Req): |         if isinstance(mixed, crypto.X509Req): | ||||||
|             self._obj = mixed |             self._obj = mixed | ||||||
|         else: |         else: | ||||||
|             raise ValueError("Can't parse %s as X.509 certificate signing request!" % mixed) |             raise ValueError("Can't parse %s (%s) as X.509 certificate signing request!" % (mixed, type(mixed))) | ||||||
|  |  | ||||||
|         assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump())) |         assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump())) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def signable(self): |     def signable(self): | ||||||
|         for key, value, data in self.extensions: |         for key, value, data in self.extensions: | ||||||
|             if key not in EXTENSION_WHITELIST: |             if key not in constants.EXTENSION_WHITELIST: | ||||||
|                 return False |                 return False | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
| @@ -243,6 +247,11 @@ class Request(CertificateBase): | |||||||
|  |  | ||||||
|  |  | ||||||
| class Certificate(CertificateBase): | class Certificate(CertificateBase): | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def suggested_filename(self): | ||||||
|  |         return self.common_name + ".crt" | ||||||
|  |  | ||||||
|     def __init__(self, mixed): |     def __init__(self, mixed): | ||||||
|         self.buf = NotImplemented |         self.buf = NotImplemented | ||||||
|         self.path = NotImplemented |         self.path = NotImplemented | ||||||
| @@ -253,15 +262,12 @@ class Certificate(CertificateBase): | |||||||
|             _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) |             _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) | ||||||
|             self.changed = datetime.fromtimestamp(mtime) |             self.changed = datetime.fromtimestamp(mtime) | ||||||
|             mixed = mixed.read() |             mixed = mixed.read() | ||||||
|  |  | ||||||
|         if isinstance(mixed, str): |         if isinstance(mixed, str): | ||||||
|             try: |             try: | ||||||
|                 self.buf = mixed |                 self.buf = mixed | ||||||
|                 mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed) |                 mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed) | ||||||
|             except crypto.Error: |             except crypto.Error: | ||||||
|                 print("Failed to parse:", mixed) |                 raise ValueError("Failed to parse: %s" % mixed) | ||||||
|                 raise |  | ||||||
|  |  | ||||||
|         if isinstance(mixed, crypto.X509): |         if isinstance(mixed, crypto.X509): | ||||||
|             self._obj = mixed |             self._obj = mixed | ||||||
|         else: |         else: | ||||||
|   | |||||||
| @@ -1,20 +1,20 @@ | |||||||
| cffi==1.2.1 | cffi==1.2.1 | ||||||
| click==5.1 | click==5.1 | ||||||
|  | configparser==3.3.0r2 | ||||||
| cryptography==1.0 | cryptography==1.0 | ||||||
| falcon==0.3.0 | falcon==0.3.0 | ||||||
| future=0.15.2 |  | ||||||
| humanize==0.5.1 | humanize==0.5.1 | ||||||
| idna==2.0 | idna==2.0 | ||||||
|  | ipaddress==1.0.16 | ||||||
| ipsecparse==0.1.0 | ipsecparse==0.1.0 | ||||||
| Jinja2==2.8 | Jinja2==2.8 | ||||||
| ldap3==0.9.8.8 | Markdown==2.6.5 | ||||||
| MarkupSafe==0.23 | MarkupSafe==0.23 | ||||||
| pyasn1==0.1.8 | pyasn1==0.1.8 | ||||||
| pycountry==1.14 |  | ||||||
| pycparser==2.14 |  | ||||||
| pycrypto==2.6.1 | pycrypto==2.6.1 | ||||||
| pykerberos==1.1.8 | pykerberos==1.1.8 | ||||||
| pyOpenSSL==0.15.1 | pyOpenSSL==0.15.1 | ||||||
|  | python-ldap==2.4.10 | ||||||
| python-mimeparse==0.1.4 | python-mimeparse==0.1.4 | ||||||
| requests==2.2.1 | requests==2.2.1 | ||||||
| setproctitle==1.1.9 | setproctitle==1.1.9 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						| @@ -36,7 +36,7 @@ setup( | |||||||
|     ], |     ], | ||||||
|     include_package_data = True, |     include_package_data = True, | ||||||
|     package_data={ |     package_data={ | ||||||
|         "certidude": ["certidude/templates/*.html"], |         "certidude": ["certidude/templates/*"], | ||||||
|     }, |     }, | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         "Development Status :: 4 - Beta", |         "Development Status :: 4 - Beta", | ||||||
|   | |||||||