mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Several changes
* OCSP workaround for StrongSwan * Machine attributes framework * Scripting support * Default to nginx frontend
This commit is contained in:
		| @@ -8,6 +8,7 @@ import click | |||||||
| import hashlib | import hashlib | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from time import sleep | from time import sleep | ||||||
|  | from xattr import listxattr, getxattr | ||||||
| from certidude import authority, mailer | from certidude import authority, mailer | ||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.user import User | from certidude.user import User | ||||||
| @@ -32,7 +33,6 @@ class SessionResource(object): | |||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         import xattr |  | ||||||
|  |  | ||||||
|         def serialize_requests(g): |         def serialize_requests(g): | ||||||
|             for common_name, path, buf, obj, server in g(): |             for common_name, path, buf, obj, server in g(): | ||||||
| @@ -50,7 +50,7 @@ class SessionResource(object): | |||||||
|                 # Extract certificate tags from filesystem |                 # Extract certificate tags from filesystem | ||||||
|                 try: |                 try: | ||||||
|                     tags = [] |                     tags = [] | ||||||
|                     for tag in xattr.getxattr(path, "user.xdg.tags").split(","): |                     for tag in getxattr(path, "user.xdg.tags").split(","): | ||||||
|                         if "=" in tag: |                         if "=" in tag: | ||||||
|                             k, v = tag.split("=", 1) |                             k, v = tag.split("=", 1) | ||||||
|                         else: |                         else: | ||||||
| @@ -59,12 +59,17 @@ class SessionResource(object): | |||||||
|                 except IOError: # No such attribute(s) |                 except IOError: # No such attribute(s) | ||||||
|                     tags = None |                     tags = None | ||||||
|  |  | ||||||
|  |                 attributes = {} | ||||||
|  |                 for key in listxattr(path): | ||||||
|  |                     if key.startswith("user.machine."): | ||||||
|  |                         attributes[key[13:]] = getxattr(path, key) | ||||||
|  |  | ||||||
|                 # Extract lease information from filesystem |                 # Extract lease information from filesystem | ||||||
|                 try: |                 try: | ||||||
|                     last_seen = datetime.strptime(xattr.getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") |                     last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") | ||||||
|                     lease = dict( |                     lease = dict( | ||||||
|                         inner_address = xattr.getxattr(path, "user.lease.inner_address"), |                         inner_address = getxattr(path, "user.lease.inner_address"), | ||||||
|                         outer_address = xattr.getxattr(path, "user.lease.outer_address"), |                         outer_address = getxattr(path, "user.lease.outer_address"), | ||||||
|                         last_seen = last_seen, |                         last_seen = last_seen, | ||||||
|                         age = datetime.utcnow() - last_seen |                         age = datetime.utcnow() - last_seen | ||||||
|                     ) |                     ) | ||||||
| @@ -80,7 +85,8 @@ class SessionResource(object): | |||||||
|                     expires = obj.not_valid_after, |                     expires = obj.not_valid_after, | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), |                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||||
|                     lease = lease, |                     lease = lease, | ||||||
|                     tags = tags |                     tags = tags, | ||||||
|  |                     attributes = attributes or None, | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|         if req.context.get("user").is_admin(): |         if req.context.get("user").is_admin(): | ||||||
| @@ -160,7 +166,7 @@ import ipaddress | |||||||
| class NormalizeMiddleware(object): | class NormalizeMiddleware(object): | ||||||
|     def process_request(self, req, resp, *args): |     def process_request(self, req, resp, *args): | ||||||
|         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" |         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" | ||||||
|         req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) |         req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0].decode("utf-8")) | ||||||
|  |  | ||||||
|     def process_response(self, req, resp, resource=None): |     def process_response(self, req, resp, resource=None): | ||||||
|         # wtf falcon?! |         # wtf falcon?! | ||||||
| @@ -181,6 +187,7 @@ def certidude_app(log_handlers=[]): | |||||||
|  |  | ||||||
|     app = falcon.API(middleware=NormalizeMiddleware()) |     app = falcon.API(middleware=NormalizeMiddleware()) | ||||||
|     app.req_options.auto_parse_form_urlencoded = True |     app.req_options.auto_parse_form_urlencoded = True | ||||||
|  |     #app.req_options.strip_url_path_trailing_slash = False | ||||||
|  |  | ||||||
|     # Certificate authority API calls |     # Certificate authority API calls | ||||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) |     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||||
| @@ -194,7 +201,7 @@ def certidude_app(log_handlers=[]): | |||||||
|         app.add_route("/api/token/", TokenResource()) |         app.add_route("/api/token/", TokenResource()) | ||||||
|  |  | ||||||
|     # Extended attributes for scripting etc. |     # Extended attributes for scripting etc. | ||||||
|     app.add_route("/api/signed/{cn}/attr/", AttributeResource()) |     app.add_route("/api/signed/{cn}/attr/", AttributeResource(namespace="machine")) | ||||||
|     app.add_route("/api/signed/{cn}/script/", ScriptResource()) |     app.add_route("/api/signed/{cn}/script/", ScriptResource()) | ||||||
|  |  | ||||||
|     # API calls used by pushed events on the JS end |     # API calls used by pushed events on the JS end | ||||||
| @@ -215,18 +222,13 @@ def certidude_app(log_handlers=[]): | |||||||
|         from .scep import SCEPResource |         from .scep import SCEPResource | ||||||
|         app.add_route("/api/scep/", SCEPResource()) |         app.add_route("/api/scep/", SCEPResource()) | ||||||
|  |  | ||||||
|     if config.OCSP_SUBNETS: |  | ||||||
|         from .ocsp import OCSPResource |  | ||||||
|         app.add_route("/api/ocsp/", OCSPResource()) |  | ||||||
|  |  | ||||||
|     # Add sink for serving static files |     # Add sink for serving static files | ||||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) |     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) | ||||||
|  |  | ||||||
|     def log_exceptions(ex, req, resp, params): |     if config.OCSP_SUBNETS: | ||||||
|         logger.debug("Caught exception: %s" % ex) |         from .ocsp import OCSPResource | ||||||
|         raise ex |         app.add_sink(OCSPResource(), prefix="/api/ocsp") | ||||||
|  |  | ||||||
|     app.add_error_handler(Exception, log_exceptions) |  | ||||||
|  |  | ||||||
|     # Set up log handlers |     # Set up log handlers | ||||||
|     if config.LOGGING_BACKEND == "sql": |     if config.LOGGING_BACKEND == "sql": | ||||||
|   | |||||||
| @@ -1,14 +1,24 @@ | |||||||
|  | import click | ||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| from ipaddress import ip_address | import re | ||||||
|  | from xattr import setxattr, listxattr, removexattr | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from certidude import config, authority | from certidude import config, authority, push | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize, csrf_protection | ||||||
|  | from certidude.firewall import whitelist_subject | ||||||
|  | from certidude.auth import login_required, login_optional, authorize_admin | ||||||
|  | from ipaddress import ip_address | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| class AttributeResource(object): | class AttributeResource(object): | ||||||
|  |     def __init__(self, namespace): | ||||||
|  |         self.namespace = namespace | ||||||
|  |  | ||||||
|     @serialize |     @serialize | ||||||
|  |     @login_required | ||||||
|  |     @authorize_admin | ||||||
|     def on_get(self, req, resp, cn): |     def on_get(self, req, resp, cn): | ||||||
|         """ |         """ | ||||||
|         Return extended attributes stored on the server. |         Return extended attributes stored on the server. | ||||||
| @@ -17,22 +27,32 @@ class AttributeResource(object): | |||||||
|         Results made available only to lease IP address. |         Results made available only to lease IP address. | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             path, buf, cert, attribs = authority.get_attributes(cn) |             path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace) | ||||||
|         except IOError: |         except IOError: | ||||||
|             raise falcon.HTTPNotFound() |             raise falcon.HTTPNotFound() | ||||||
|         else: |         else: | ||||||
|             try: |  | ||||||
|                 whitelist = ip_address(attribs.get("user").get("lease").get("inner_address").decode("ascii")) |  | ||||||
|             except AttributeError: # TODO: probably race condition |  | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", |  | ||||||
|                     "Attributes only accessible to the machine") |  | ||||||
|  |  | ||||||
|             if req.context.get("remote_addr") != whitelist: |  | ||||||
|                 logger.info("Attribute access denied from %s, expected %s for %s", |  | ||||||
|                     req.context.get("remote_addr"), |  | ||||||
|                     whitelist, |  | ||||||
|                     cn) |  | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", |  | ||||||
|                     "Attributes only accessible to the machine") |  | ||||||
|  |  | ||||||
|             return attribs |             return attribs | ||||||
|  |  | ||||||
|  |     @csrf_protection | ||||||
|  |     @whitelist_subject # TODO: sign instead | ||||||
|  |     def on_post(self, req, resp, cn): | ||||||
|  |         try: | ||||||
|  |             path, buf, cert = authority.get_signed(cn) | ||||||
|  |         except IOError: | ||||||
|  |             raise falcon.HTTPNotFound() | ||||||
|  |         else: | ||||||
|  |             for key in req.params: | ||||||
|  |                 if not re.match("[a-z0-9_\.]+$", key): | ||||||
|  |                     raise falcon.HTTPBadRequest("Invalid key") | ||||||
|  |             valid = set() | ||||||
|  |             for key, value in req.params.items(): | ||||||
|  |                 identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") | ||||||
|  |                 setxattr(path, identifier, value.encode("utf-8")) | ||||||
|  |                 valid.add(identifier) | ||||||
|  |             for key in listxattr(path): | ||||||
|  |                 if not key.startswith("user.%s." % self.namespace): | ||||||
|  |                     continue | ||||||
|  |                 if key not in valid: | ||||||
|  |                     removexattr(path, key) | ||||||
|  |             push.publish("attribute-update", cn) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| from __future__ import unicode_literals, division, absolute_import, print_function |  | ||||||
| import click | import click | ||||||
| import hashlib | import hashlib | ||||||
| import os | import os | ||||||
| @@ -14,26 +13,35 @@ from oscrypto import keys, asymmetric, symmetric | |||||||
| from oscrypto.errors import SignatureError | from oscrypto.errors import SignatureError | ||||||
|  |  | ||||||
| class OCSPResource(object): | class OCSPResource(object): | ||||||
|     def on_post(self, req, resp): |     def __call__(self, req, resp): | ||||||
|  |         if req.method == "GET": | ||||||
|  |             _, _, _, tail = req.path.split("/", 3) | ||||||
|  |             body = b64decode(tail) | ||||||
|  |         elif req.method == "POST": | ||||||
|  |             body = req.stream.read(req.content_length or 0) | ||||||
|  |         else: | ||||||
|  |             raise falcon.HTTPMethodNotAllowed() | ||||||
|  |  | ||||||
|         fh = open(config.AUTHORITY_CERTIFICATE_PATH) |         fh = open(config.AUTHORITY_CERTIFICATE_PATH) | ||||||
|         server_certificate = asymmetric.load_certificate(fh.read()) |         server_certificate = asymmetric.load_certificate(fh.read()) | ||||||
|         fh.close() |         fh.close() | ||||||
|  |  | ||||||
|         ocsp_req = ocsp.OCSPRequest.load(req.stream.read()) |         ocsp_req = ocsp.OCSPRequest.load(body) | ||||||
|         print(ocsp_req["tbs_request"].native) |  | ||||||
|  |  | ||||||
|         now = datetime.now(timezone.utc) |         now = datetime.now(timezone.utc) | ||||||
|         response_extensions = [] |         response_extensions = [] | ||||||
|  |  | ||||||
|         for ext in ocsp_req["tbs_request"]["request_extensions"]: |         try: | ||||||
|             if ext["extn_id"] == "nonce": |             for ext in ocsp_req["tbs_request"]["request_extensions"]: | ||||||
|                 response_extensions.append( |                 if ext["extn_id"].native == "nonce": | ||||||
|                     ocsp.ResponseDataExtension({ |                     response_extensions.append( | ||||||
|                         'extn_id': "nonce", |                         ocsp.ResponseDataExtension({ | ||||||
|                         'critical': False, |                             'extn_id': "nonce", | ||||||
|                         'extn_value': ext["extn_value"] |                             'critical': False, | ||||||
|                     }) |                             'extn_value': ext["extn_value"] | ||||||
|                 ) |                         }) | ||||||
|  |                     ) | ||||||
|  |         except ValueError: # https://github.com/wbond/asn1crypto/issues/56 | ||||||
|  |             pass | ||||||
|  |  | ||||||
|         responses = [] |         responses = [] | ||||||
|         for item in ocsp_req["tbs_request"]["request_list"]: |         for item in ocsp_req["tbs_request"]["request_list"]: | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| from __future__ import unicode_literals, division, absolute_import, print_function |  | ||||||
| import click | import click | ||||||
| import hashlib | import hashlib | ||||||
| import os | import os | ||||||
|   | |||||||
| @@ -1,21 +1,37 @@ | |||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| from certidude import config, authority | from certidude import const, config, authority | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
| from jinja2 import Environment, FileSystemLoader | from jinja2 import Environment, FileSystemLoader | ||||||
|  | from certidude.firewall import whitelist_subject | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) | env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) | ||||||
|  |  | ||||||
| class ScriptResource(): | class ScriptResource(): | ||||||
|  |     @whitelist_subject | ||||||
|     def on_get(self, req, resp, cn): |     def on_get(self, req, resp, cn): | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             path, buf, cert, attribs = authority.get_attributes(cn) |             path, buf, cert, attribs = authority.get_attributes(cn) | ||||||
|         except IOError: |         except IOError: | ||||||
|             raise falcon.HTTPNotFound() |             raise falcon.HTTPNotFound() | ||||||
|         else: |         else: | ||||||
|             resp.set_header("Content-Type", "text/x-shellscript") |             script = config.SCRIPT_DEFAULT | ||||||
|             resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs) |             tags = [] | ||||||
|  |             for tag in attribs.get("user").get("xdg").get("tags").split(","): | ||||||
|  |                 if "=" in tag: | ||||||
|  |                     k, v = tag.split("=", 1) | ||||||
|  |                 else: | ||||||
|  |                     k, v = "other", tag | ||||||
|  |                 if k == "script": | ||||||
|  |                     script = v | ||||||
|  |                 tags.append(dict(id=tag, key=k, value=v)) | ||||||
|  |  | ||||||
|  |             resp.set_header("Content-Type", "text/x-shellscript") | ||||||
|  |             resp.body = env.get_template(script).render( | ||||||
|  |                 authority_name=const.FQDN, | ||||||
|  |                 common_name=cn, | ||||||
|  |                 tags=tags, | ||||||
|  |                 attributes=attribs.get("user").get("machine")) | ||||||
|  |             logger.info("Served script %s for %s at %s" % (script, cn, req.context["remote_addr"])) | ||||||
|         # TODO: Assert time is within reasonable range |         # TODO: Assert time is within reasonable range | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| from __future__ import unicode_literals, division, absolute_import, print_function | from __future__ import division, absolute_import, print_function | ||||||
| import click | import click | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
| @@ -57,17 +57,19 @@ def get_revoked(serial): | |||||||
|             datetime.utcfromtimestamp(os.stat(path).st_ctime) |             datetime.utcfromtimestamp(os.stat(path).st_ctime) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_attributes(cn): | def get_attributes(cn, namespace=None): | ||||||
|     path, buf, cert = get_signed(cn) |     path, buf, cert = get_signed(cn) | ||||||
|     attribs = dict() |     attribs = dict() | ||||||
|     for key in listxattr(path): |     for key in listxattr(path): | ||||||
|         if not key.startswith("user."): |         if not key.startswith("user."): | ||||||
|             continue |             continue | ||||||
|  |         if namespace and not key.startswith("user.%s." % namespace): | ||||||
|  |             continue | ||||||
|         value = getxattr(path, key) |         value = getxattr(path, key) | ||||||
|         current = attribs |         current = attribs | ||||||
|         if "." in key: |         if "." in key: | ||||||
|             namespace, key = key.rsplit(".", 1) |             prefix, key = key.rsplit(".", 1) | ||||||
|             for component in namespace.split("."): |             for component in prefix.split("."): | ||||||
|                 if component not in current: |                 if component not in current: | ||||||
|                     current[component] = dict() |                     current[component] = dict() | ||||||
|                 current = current[component] |                 current = current[component] | ||||||
| @@ -324,7 +326,6 @@ def sign(common_name, overwrite=False): | |||||||
| def _sign(csr, buf, overwrite=False): | def _sign(csr, buf, overwrite=False): | ||||||
|     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") |     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") | ||||||
|     assert isinstance(csr, x509.CertificateSigningRequest) |     assert isinstance(csr, x509.CertificateSigningRequest) | ||||||
|     from xattr import getxattr, listxattr, setxattr |  | ||||||
|  |  | ||||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) |     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||||
|     cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) |     cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) | ||||||
|   | |||||||
| @@ -1004,24 +1004,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | |||||||
|     static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") |     static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") | ||||||
|     certidude_path = sys.argv[0] |     certidude_path = sys.argv[0] | ||||||
|  |  | ||||||
|     # Push server config generation |     click.echo("Generating: %s" % nginx_config.name) | ||||||
|     if os.path.exists("/etc/nginx"): |     nginx_config.write(env.get_template("server/nginx.conf").render(vars())) | ||||||
|         listen = "127.0.1.1" |     nginx_config.close() | ||||||
|         port = "8080" |     if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): | ||||||
|         click.echo("Generating: %s" % nginx_config.name) |         os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") | ||||||
|         nginx_config.write(env.get_template("server/nginx.conf").render(vars())) |         click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) | ||||||
|         nginx_config.close() |     if os.path.exists("/etc/nginx/sites-enabled/default"): | ||||||
|         if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): |         os.unlink("/etc/nginx/sites-enabled/default") | ||||||
|             os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") |     os.system("service nginx restart") | ||||||
|             click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) |  | ||||||
|         if os.path.exists("/etc/nginx/sites-enabled/default"): |  | ||||||
|             os.unlink("/etc/nginx/sites-enabled/default") |  | ||||||
|         os.system("service nginx restart") |  | ||||||
|     else: |  | ||||||
|         click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") |  | ||||||
|         click.echo("Remember to install/configure nchan capable nginx instead of regular nginx!") |  | ||||||
|         listen = "0.0.0.0" |  | ||||||
|         port = "80" |  | ||||||
|  |  | ||||||
|     if os.path.exists("/etc/systemd"): |     if os.path.exists("/etc/systemd"): | ||||||
|         if os.path.exists("/etc/systemd/system/certidude.service"): |         if os.path.exists("/etc/systemd/system/certidude.service"): | ||||||
| @@ -1284,16 +1275,19 @@ def certidude_cron(): | |||||||
|  |  | ||||||
| @click.command("serve", help="Run server") | @click.command("serve", help="Run server") | ||||||
| @click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler") | @click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler") | ||||||
| @click.option("-p", "--port", default=80, help="Listen port") | @click.option("-p", "--port", default=8080, help="Listen port") | ||||||
| @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") | @click.option("-l", "--listen", default="127.0.0.1", help="Listen address") | ||||||
| @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") | @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") | ||||||
| def certidude_serve(port, listen, fork, exit_handler): | def certidude_serve(port, listen, fork, exit_handler): | ||||||
|     import pwd |     import pwd | ||||||
|     from setproctitle import setproctitle |     from setproctitle import setproctitle | ||||||
|     from certidude.signer import SignServer |     from certidude.signer import SignServer | ||||||
|     from certidude import authority, const |     from certidude import authority, const | ||||||
|     click.echo("Using configuration from: %s" % const.CONFIG_PATH) |  | ||||||
|  |  | ||||||
|  |     if port == 80: | ||||||
|  |         click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!") | ||||||
|  |  | ||||||
|  |     click.echo("Using configuration from: %s" % const.CONFIG_PATH) | ||||||
|  |  | ||||||
|     log_handlers = [] |     log_handlers = [] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -98,4 +98,4 @@ TOKEN_SECRET = cp.get("token", "secret") | |||||||
|  |  | ||||||
| # The API call for looking up scripts uses following directory as root | # The API call for looking up scripts uses following directory as root | ||||||
| SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") | SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") | ||||||
| SCRIPT_DEFAULT = "openwrt.sh" | SCRIPT_DEFAULT = "default.sh" | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
|  |  | ||||||
|  | import falcon | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
| @@ -20,7 +21,7 @@ def whitelist_subnets(subnets): | |||||||
|                     req.env["PATH_INFO"], |                     req.env["PATH_INFO"], | ||||||
|                     req.context.get("user", "unauthenticated user"), |                     req.context.get("user", "unauthenticated user"), | ||||||
|                     req.context.get("remote_addr")) |                     req.context.get("remote_addr")) | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) |                 raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) | ||||||
|  |  | ||||||
|             return func(self, req, resp, *args, **kwargs) |             return func(self, req, resp, *args, **kwargs) | ||||||
|         return wrapped |         return wrapped | ||||||
| @@ -39,3 +40,19 @@ def whitelist_content_types(*content_types): | |||||||
|         return wrapped |         return wrapped | ||||||
|     return wrapper |     return wrapper | ||||||
|  |  | ||||||
|  | def whitelist_subject(func): | ||||||
|  |     def wrapped(self, req, resp, cn, *args, **kwargs): | ||||||
|  |         from ipaddress import ip_address | ||||||
|  |         from certidude import authority | ||||||
|  |         from xattr import getxattr | ||||||
|  |         try: | ||||||
|  |             path, buf, cert = authority.get_signed(cn) | ||||||
|  |         except IOError: | ||||||
|  |             raise falcon.HTTPNotFound() | ||||||
|  |         else: | ||||||
|  |             inner_address = getxattr(path, "user.lease.inner_address").decode("ascii") | ||||||
|  |             if req.context.get("remote_addr") != ip_address(inner_address): | ||||||
|  |                 raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) | ||||||
|  |             return func(self, req, resp, cn, *args, **kwargs) | ||||||
|  |     return wrapped | ||||||
|  |  | ||||||
|   | |||||||
| @@ -183,7 +183,8 @@ pre { | |||||||
|     padding-bottom: 2px; |     padding-bottom: 2px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .tags .tag { | .tags .tag, | ||||||
|  | .attributes .attribute { | ||||||
|     display: inline; |     display: inline; | ||||||
|     background-size: 24px; |     background-size: 24px; | ||||||
|     background-position: 0 4px; |     background-position: 0 4px; | ||||||
| @@ -195,6 +196,15 @@ pre { | |||||||
|     white-space: nowrap; |     white-space: nowrap; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .attribute { | ||||||
|  |     opacity: 0.25; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .attribute:hover { | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| select { | select { | ||||||
|    -webkit-appearance: none; |    -webkit-appearance: none; | ||||||
|    -moz-appearance: none; |    -moz-appearance: none; | ||||||
| @@ -205,7 +215,8 @@ select { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); } | .icon.tag, | ||||||
|  | .icon.attribute { background-image: url("../img/iconmonstr-tag-3.svg"); } | ||||||
|  |  | ||||||
| .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } | .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } | ||||||
| .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } | .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } | ||||||
| @@ -227,5 +238,12 @@ select { | |||||||
|  |  | ||||||
| .icon.upload { background-image: url("../img/iconmonstr-upload-17.svg"); } | .icon.upload { background-image: url("../img/iconmonstr-upload-17.svg"); } | ||||||
|  |  | ||||||
|  | .icon.dist, | ||||||
|  | .icon.kernel { background-image: url("../img/iconmonstr-linux-os-1.svg"); } | ||||||
|  | .icon.if { background-image: url("../img/iconmonstr-sitemap-5.svg"); } | ||||||
|  | .icon.cpu, | ||||||
|  | .icon.mem, | ||||||
|  | .icon.ram { background-image: url("../img/iconmonstr-cpu-1.svg"); } | ||||||
|  |  | ||||||
| /* Make sure this is the last one */ | /* Make sure this is the last one */ | ||||||
| .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} | .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-cpu-1.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								certidude/static/img/iconmonstr-cpu-1.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 17c0 1.104-.896 2-2 2h-11c-1.104 0-2-.896-2-2v-11c0-1.104.896-2 2-2h11c1.104 0 2 .896 2 2v11zm-11 3v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm-8-20v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm4 15h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1zm-20 8h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1z"/></svg> | ||||||
| After Width: | Height: | Size: 507 B | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-linux-os-1.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								certidude/static/img/iconmonstr-linux-os-1.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.581 19.049c-.55-.446-.336-1.431-.907-1.917.553-3.365-.997-6.331-2.845-8.232-1.551-1.595-1.051-3.147-1.051-4.49 0-2.146-.881-4.41-3.55-4.41-2.853 0-3.635 2.38-3.663 3.738-.068 3.262.659 4.11-1.25 6.484-2.246 2.793-2.577 5.579-2.07 7.057-.237.276-.557.582-1.155.835-1.652.72-.441 1.925-.898 2.78-.13.243-.192.497-.192.74 0 .75.596 1.399 1.679 1.302 1.461-.13 2.809.905 3.681.905.77 0 1.402-.438 1.696-1.041 1.377-.339 3.077-.296 4.453.059.247.691.917 1.141 1.662 1.141 1.631 0 1.945-1.849 3.816-2.475.674-.225 1.013-.879 1.013-1.488 0-.39-.139-.761-.419-.988zm-9.147-10.465c-.319 0-.583-.258-1-.568-.528-.392-1.065-.618-1.059-1.03 0-.283.379-.37.869-.681.526-.333.731-.671 1.249-.671.53 0 .69.268 1.41.579.708.307 1.201.427 1.201.773 0 .355-.741.609-1.158.868-.613.378-.928.73-1.512.73zm1.665-5.215c.882.141.981 1.691.559 2.454l-.355-.145c.184-.543.181-1.437-.435-1.494-.391-.036-.643.48-.697.922-.153-.064-.32-.11-.523-.127.062-.923.658-1.737 1.451-1.61zm-3.403.331c.676-.168 1.075.618 1.078 1.435l-.31.19c-.042-.343-.195-.897-.579-.779-.411.128-.344 1.083-.115 1.279l-.306.17c-.42-.707-.419-2.133.232-2.295zm-2.115 19.243c-1.963-.893-2.63-.69-3.005-.69-.777 0-1.031-.579-.739-1.127.248-.465.171-.952.11-1.343-.094-.599-.111-.794.478-1.052.815-.346 1.177-.791 1.447-1.124.758-.937 1.523.537 2.15 1.85.407.851 1.208 1.282 1.455 2.225.227.871-.71 1.801-1.896 1.261zm6.987-1.874c-1.384.673-3.147.982-4.466.299-.195-.563-.507-.927-.843-1.293.539-.142.939-.814.46-1.489-.511-.721-1.555-1.224-2.61-2.04-.987-.763-1.299-2.644.045-4.746-.655 1.862-.272 3.578.057 4.069.068-.988.146-2.638 1.496-4.615.681-.998.691-2.316.706-3.14l.62.424c.456.337.838.708 1.386.708.81 0 1.258-.466 1.882-.853.244-.15.613-.302.923-.513.52 2.476 2.674 5.454 2.795 7.15.501-1.032-.142-3.514-.142-3.514.842 1.285.909 2.356.946 3.67.589.241 1.221.869 1.279 1.696l-.245-.028c-.126-.919-2.607-2.269-2.83-.539-1.19.181-.757 2.066-.997 3.288-.11.559-.314 1.001-.462 1.466zm4.846-.041c-.985.38-1.65 1.187-2.107 1.688-.88.966-2.044.503-2.168-.401-.131-.966.36-1.493.572-2.574.193-.987-.023-2.506.431-2.668.295 1.753 2.066 1.016 2.47.538.657 0 .712.222.859.837.092.385.219.709.578 1.09.418.447.29 1.133-.635 1.49zm-8-13.006c-.651 0-1.138-.433-1.534-.769-.203-.171.05-.487.253-.315.387.328.777.675 1.281.675.607 0 1.142-.519 1.867-.805.247-.097.388.285.143.382-.704.277-1.269.832-2.01.832z"/></svg> | ||||||
| After Width: | Height: | Size: 2.4 KiB | 
							
								
								
									
										1
									
								
								certidude/static/img/iconmonstr-sitemap-5.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								certidude/static/img/iconmonstr-sitemap-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="M16 6h-8v-6h8v6zm-10 12h-6v6h6v-6zm18 0h-6v6h6v-6zm-11-7v-3h-2v3h-9v5h2v-3h7v3h2v-3h7v3h2v-5h-9zm2 7h-6v6h6v-6z"/></svg> | ||||||
| After Width: | Height: | Size: 212 B | 
| @@ -181,6 +181,24 @@ function onTagUpdated(e) { | |||||||
|     }) |     }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function onAttributeUpdated(e) { | ||||||
|  |     var cn = e.data; | ||||||
|  |     console.log("Attributes updated", cn); | ||||||
|  |     $.ajax({ | ||||||
|  |         method: "GET", | ||||||
|  |         url: "/api/signed/" + cn + "/attr/", | ||||||
|  |         dataType: "json", | ||||||
|  |         success:function(attributes, status, xhr) { | ||||||
|  |             console.info("Updated", cn, "attributes", attributes); | ||||||
|  |             $(".attributes[data-cn='" + cn + "']").html( | ||||||
|  |                 nunjucks.render('views/attributes.html', { | ||||||
|  |                     certificate: { | ||||||
|  |                         common_name: cn, | ||||||
|  |                         attributes:attributes }})); | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
| $(document).ready(function() { | $(document).ready(function() { | ||||||
|     console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); |     console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
| @@ -228,6 +246,7 @@ $(document).ready(function() { | |||||||
|                 source.addEventListener("request-signed", onRequestSigned); |                 source.addEventListener("request-signed", onRequestSigned); | ||||||
|                 source.addEventListener("certificate-revoked", onCertificateRevoked); |                 source.addEventListener("certificate-revoked", onCertificateRevoked); | ||||||
|                 source.addEventListener("tag-update", onTagUpdated); |                 source.addEventListener("tag-update", onTagUpdated); | ||||||
|  |                 source.addEventListener("attribute-update", onAttributeUpdated); | ||||||
|  |  | ||||||
|                 console.info("Swtiching to requests section"); |                 console.info("Swtiching to requests section"); | ||||||
|                 $("section").hide(); |                 $("section").hide(); | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								certidude/static/views/attributes.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								certidude/static/views/attributes.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | {% for key, value in certificate.attributes %} | ||||||
|  | <span class="attribute icon {{ key | replace('.', ' ') }}">{{ value }}</span> | ||||||
|  | {% endfor %} | ||||||
| @@ -61,4 +61,8 @@ | |||||||
|     <div class="status"> |     <div class="status"> | ||||||
|     {% include 'views/status.html' %} |     {% include 'views/status.html' %} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  |     <div class="attributes" data-cn="{{ certificate.common_name }}"> | ||||||
|  |         {% include 'views/attributes.html' %} | ||||||
|  |     </div> | ||||||
| </li> | </li> | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								certidude/templates/script/default.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								certidude/templates/script/default.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | #!/bin/sh | ||||||
|  |  | ||||||
|  | # Tags: | ||||||
|  | {% for tag in tags %} | ||||||
|  | # {{ tag }} | ||||||
|  | {% endfor %} | ||||||
|  |  | ||||||
|  | curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\ | ||||||
|  | dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\ | ||||||
|  | dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\ | ||||||
|  | kernel=$(uname -sr)&\ | ||||||
|  | dist=$(lsb_release -si) $(lsb_release -sr)&\ | ||||||
|  | cpu=$(cat /proc/cpuinfo  | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\ | ||||||
|  | mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\ | ||||||
|  | $(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)" | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| #!/bin/sh | #!/bin/sh | ||||||
|  |  | ||||||
| # This script can executed on a preconfigured OpenWrt box | # This script can be executed on a preconfigured OpenWrt box | ||||||
| # https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html | # https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html | ||||||
|  |  | ||||||
| # Password protected wireless area | # Password protected wireless area | ||||||
| @@ -10,13 +10,13 @@ for band in 2ghz 5ghz; do | |||||||
|     uci set wireless.lan$band.mode=ap |     uci set wireless.lan$band.mode=ap | ||||||
|     uci set wireless.lan$band.device=radio$band |     uci set wireless.lan$band.device=radio$band | ||||||
|     uci set wireless.lan$band.encryption=psk2 |     uci set wireless.lan$band.encryption=psk2 | ||||||
|     {% if attributes.protected and attributes.protected.ssid %} |     {% if attributes.wireless.protected and attributes.wireless.protected.ssid %} | ||||||
|     uci set wireless.lan$band.ssid={{ attrbutes.protected.ssid }} |     uci set wireless.lan$band.ssid={{ attrbutes.wireless.protected.ssid }} | ||||||
|     {% else %} |     {% else %} | ||||||
|     uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected |     uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if attributes.protected and attributes.protected.psk %} |     {% if attributes.wireless.protected and attributes.wireless.protected.psk %} | ||||||
|     uci set wireless.lan$band.key={{ attributes.protected.psk }} |     uci set wireless.lan$band.key={{ attributes.wireless.protected.psk }} | ||||||
|     {% else %} |     {% else %} | ||||||
|     uci set wireless.lan$band.key=salakala |     uci set wireless.lan$band.key=salakala | ||||||
|     {% endif %} |     {% endif %} | ||||||
| @@ -29,8 +29,8 @@ for band in 2ghz 5ghz; do | |||||||
|     uci set wireless.guest$band.mode=ap |     uci set wireless.guest$band.mode=ap | ||||||
|     uci set wireless.guest$band.device=radio$band |     uci set wireless.guest$band.device=radio$band | ||||||
|     uci set wireless.guest$band.encryption=none |     uci set wireless.guest$band.encryption=none | ||||||
|     {% if attributes.public and attributes.public.ssid %} |     {% if attributes.wireless.public and attributes.wireless.public.ssid %} | ||||||
|     uci set wireless.guest$band.ssid={{ attrbutes.public.ssid }} |     uci set wireless.guest$band.ssid={{ attrbutes.wireless.public.ssid }} | ||||||
|     {% else %} |     {% else %} | ||||||
|     uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public |     uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public | ||||||
|     {% endif %} |     {% endif %} | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ server { | |||||||
|     root {{static_path}}; |     root {{static_path}}; | ||||||
|  |  | ||||||
|     location /api/ { |     location /api/ { | ||||||
|         proxy_pass http://127.0.1.1{% if port != 80 %}:{{ port }}{% endif %}/api/; |         proxy_pass http://127.0.1.1:8080/api/; | ||||||
|         proxy_set_header Host $host; |         proxy_set_header Host $host; | ||||||
|         proxy_set_header X-Real-IP $remote_addr; |         proxy_set_header X-Real-IP $remote_addr; | ||||||
|         proxy_connect_timeout 600; |         proxy_connect_timeout 600; | ||||||
|   | |||||||
| @@ -7,8 +7,7 @@ Environment=PYTHON_EGG_CACHE=/tmp/.cache | |||||||
| PIDFile=/run/certidude/server.pid | PIDFile=/run/certidude/server.pid | ||||||
| ExecReload=/bin/kill -s HUP $MAINPID | ExecReload=/bin/kill -s HUP $MAINPID | ||||||
| ExecStop=/bin/kill -s TERM $MAINPID | ExecStop=/bin/kill -s TERM $MAINPID | ||||||
| ExecStart={{ certidude_path }} serve {% if listen %} -l {{listen}}{% endif %}{% if port %} -p {{port}}{% endif %} | ExecStart={{ certidude_path }} serve | ||||||
|  |  | ||||||
|  |  | ||||||
| [Install] | [Install] | ||||||
| WantedBy=multi-user.target | WantedBy=multi-user.target | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user