1
0
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:
2017-07-05 18:22:03 +03:00
parent d08a3f9f92
commit 9b5511212e
20 changed files with 211 additions and 93 deletions

View File

@@ -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":

View File

@@ -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)

View File

@@ -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"]:

View File

@@ -1,4 +1,3 @@
from __future__ import unicode_literals, division, absolute_import, print_function
import click
import hashlib
import os

View File

@@ -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