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 | ||||
| from datetime import datetime | ||||
| from time import sleep | ||||
| from xattr import listxattr, getxattr | ||||
| from certidude import authority, mailer | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.user import User | ||||
| @@ -32,7 +33,6 @@ class SessionResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     def on_get(self, req, resp): | ||||
|         import xattr | ||||
|  | ||||
|         def serialize_requests(g): | ||||
|             for common_name, path, buf, obj, server in g(): | ||||
| @@ -50,7 +50,7 @@ class SessionResource(object): | ||||
|                 # Extract certificate tags from filesystem | ||||
|                 try: | ||||
|                     tags = [] | ||||
|                     for tag in xattr.getxattr(path, "user.xdg.tags").split(","): | ||||
|                     for tag in getxattr(path, "user.xdg.tags").split(","): | ||||
|                         if "=" in tag: | ||||
|                             k, v = tag.split("=", 1) | ||||
|                         else: | ||||
| @@ -59,12 +59,17 @@ class SessionResource(object): | ||||
|                 except IOError: # No such attribute(s) | ||||
|                     tags = None | ||||
|  | ||||
|                 attributes = {} | ||||
|                 for key in listxattr(path): | ||||
|                     if key.startswith("user.machine."): | ||||
|                         attributes[key[13:]] = getxattr(path, key) | ||||
|  | ||||
|                 # Extract lease information from filesystem | ||||
|                 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( | ||||
|                         inner_address = xattr.getxattr(path, "user.lease.inner_address"), | ||||
|                         outer_address = xattr.getxattr(path, "user.lease.outer_address"), | ||||
|                         inner_address = getxattr(path, "user.lease.inner_address"), | ||||
|                         outer_address = getxattr(path, "user.lease.outer_address"), | ||||
|                         last_seen = last_seen, | ||||
|                         age = datetime.utcnow() - last_seen | ||||
|                     ) | ||||
| @@ -80,7 +85,8 @@ class SessionResource(object): | ||||
|                     expires = obj.not_valid_after, | ||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||
|                     lease = lease, | ||||
|                     tags = tags | ||||
|                     tags = tags, | ||||
|                     attributes = attributes or None, | ||||
|                 ) | ||||
|  | ||||
|         if req.context.get("user").is_admin(): | ||||
| @@ -160,7 +166,7 @@ import ipaddress | ||||
| class NormalizeMiddleware(object): | ||||
|     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")) | ||||
|         req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0].decode("utf-8")) | ||||
|  | ||||
|     def process_response(self, req, resp, resource=None): | ||||
|         # wtf falcon?! | ||||
| @@ -181,6 +187,7 @@ def certidude_app(log_handlers=[]): | ||||
|  | ||||
|     app = falcon.API(middleware=NormalizeMiddleware()) | ||||
|     app.req_options.auto_parse_form_urlencoded = True | ||||
|     #app.req_options.strip_url_path_trailing_slash = False | ||||
|  | ||||
|     # Certificate authority API calls | ||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||
| @@ -194,7 +201,7 @@ def certidude_app(log_handlers=[]): | ||||
|         app.add_route("/api/token/", TokenResource()) | ||||
|  | ||||
|     # 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()) | ||||
|  | ||||
|     # API calls used by pushed events on the JS end | ||||
| @@ -215,18 +222,13 @@ def certidude_app(log_handlers=[]): | ||||
|         from .scep import 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 | ||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) | ||||
|  | ||||
|     def log_exceptions(ex, req, resp, params): | ||||
|         logger.debug("Caught exception: %s" % ex) | ||||
|         raise ex | ||||
|  | ||||
|     app.add_error_handler(Exception, log_exceptions) | ||||
|     if config.OCSP_SUBNETS: | ||||
|         from .ocsp import OCSPResource | ||||
|         app.add_sink(OCSPResource(), prefix="/api/ocsp") | ||||
|  | ||||
|     # Set up log handlers | ||||
|     if config.LOGGING_BACKEND == "sql": | ||||
|   | ||||
| @@ -1,14 +1,24 @@ | ||||
| import click | ||||
| import falcon | ||||
| import logging | ||||
| from ipaddress import ip_address | ||||
| import re | ||||
| from xattr import setxattr, listxattr, removexattr | ||||
| from datetime import datetime | ||||
| from certidude import config, authority | ||||
| from certidude.decorators import serialize | ||||
| from certidude import config, authority, push | ||||
| 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__) | ||||
|  | ||||
| class AttributeResource(object): | ||||
|     def __init__(self, namespace): | ||||
|         self.namespace = namespace | ||||
|  | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp, cn): | ||||
|         """ | ||||
|         Return extended attributes stored on the server. | ||||
| @@ -17,22 +27,32 @@ class AttributeResource(object): | ||||
|         Results made available only to lease IP address. | ||||
|         """ | ||||
|         try: | ||||
|             path, buf, cert, attribs = authority.get_attributes(cn) | ||||
|             path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace) | ||||
|         except IOError: | ||||
|             raise falcon.HTTPNotFound() | ||||
|         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 | ||||
|  | ||||
|     @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 hashlib | ||||
| import os | ||||
| @@ -14,26 +13,35 @@ from oscrypto import keys, asymmetric, symmetric | ||||
| from oscrypto.errors import SignatureError | ||||
|  | ||||
| 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) | ||||
|         server_certificate = asymmetric.load_certificate(fh.read()) | ||||
|         fh.close() | ||||
|  | ||||
|         ocsp_req = ocsp.OCSPRequest.load(req.stream.read()) | ||||
|         print(ocsp_req["tbs_request"].native) | ||||
|  | ||||
|         ocsp_req = ocsp.OCSPRequest.load(body) | ||||
|         now = datetime.now(timezone.utc) | ||||
|         response_extensions = [] | ||||
|  | ||||
|         for ext in ocsp_req["tbs_request"]["request_extensions"]: | ||||
|             if ext["extn_id"] == "nonce": | ||||
|                 response_extensions.append( | ||||
|                     ocsp.ResponseDataExtension({ | ||||
|                         'extn_id': "nonce", | ||||
|                         'critical': False, | ||||
|                         'extn_value': ext["extn_value"] | ||||
|                     }) | ||||
|                 ) | ||||
|         try: | ||||
|             for ext in ocsp_req["tbs_request"]["request_extensions"]: | ||||
|                 if ext["extn_id"].native == "nonce": | ||||
|                     response_extensions.append( | ||||
|                         ocsp.ResponseDataExtension({ | ||||
|                             'extn_id': "nonce", | ||||
|                             'critical': False, | ||||
|                             'extn_value': ext["extn_value"] | ||||
|                         }) | ||||
|                     ) | ||||
|         except ValueError: # https://github.com/wbond/asn1crypto/issues/56 | ||||
|             pass | ||||
|  | ||||
|         responses = [] | ||||
|         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 hashlib | ||||
| import os | ||||
|   | ||||
| @@ -1,21 +1,37 @@ | ||||
| import falcon | ||||
| import logging | ||||
| from certidude import config, authority | ||||
| from certidude import const, config, authority | ||||
| from certidude.decorators import serialize | ||||
| from jinja2 import Environment, FileSystemLoader | ||||
| from certidude.firewall import whitelist_subject | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
| env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) | ||||
|  | ||||
| class ScriptResource(): | ||||
|     @whitelist_subject | ||||
|     def on_get(self, req, resp, cn): | ||||
|  | ||||
|         try: | ||||
|             path, buf, cert, attribs = authority.get_attributes(cn) | ||||
|         except IOError: | ||||
|             raise falcon.HTTPNotFound() | ||||
|         else: | ||||
|             resp.set_header("Content-Type", "text/x-shellscript") | ||||
|             resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs) | ||||
|             script = config.SCRIPT_DEFAULT | ||||
|             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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user