mirror of
https://github.com/laurivosandi/certidude
synced 2026-01-12 17:06:59 +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