pinecrypt-gateway-backend/pinecrypt/server/api/request.py

269 lines
12 KiB
Python
Raw Normal View History

import click
import falcon
import logging
import json
import hashlib
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from pinecrypt.server import const, errors, authority
from pinecrypt.server.decorators import csrf_protection, MyEncoder
from pinecrypt.server.user import DirectoryConnection
from oscrypto import asymmetric
from .utils.firewall import whitelist_subnets, whitelist_content_types, \
login_required, login_optional, authorize_admin, validate_clock_skew
logger = logging.getLogger(__name__)
"""
openssl genrsa -out test.key 1024
openssl req -new -sha256 -key test.key -out test.csr -subj "/CN=test"
curl -f -L -H "Content-type: application/pkcs10" --data-binary @test.csr \
http://ca.example.lan/api/request/?wait=yes
"""
class RequestListResource(object):
@login_optional
@whitelist_subnets(const.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10")
@validate_clock_skew
def on_post(self, req, resp):
"""
Validate and parse certificate signing request, the RESTful way
Endpoint urls
/request/?wait=yes
/request/autosign=1
/request
"""
reasons = []
body = req.stream.read(req.content_length)
try:
header, _, der_bytes = pem.unarmor(body)
csr = CertificationRequest.load(der_bytes)
except ValueError:
logger.info("Malformed certificate signing request submission from %s blocked", req.context["remote"]["addr"])
raise falcon.HTTPBadRequest(
"Bad request",
"Malformed certificate signing request")
else:
req_public_key = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
if authority.public_key.algorithm != req_public_key.algorithm:
logger.info("Attempt to submit %s based request from %s blocked, only %s allowed" % (
req_public_key.algorithm.upper(),
req.context["remote"]["addr"],
authority.public_key.algorithm.upper()))
raise falcon.HTTPBadRequest(
"Bad request",
"Unsupported key algorithm %s, expected %s" % (req_public_key.algorithm, authority.public_key.algorithm))
try:
common_name = csr["certification_request_info"]["subject"].native["common_name"]
except KeyError:
logger.info("Malformed certificate signing request without common name submitted from %s" % req.context["remote"]["addr"])
raise falcon.HTTPBadRequest(title="Bad request",description="Common name missing from certificate signing request")
"""
Determine whether autosign is allowed to overwrite already issued
certificates automatically
"""
overwrite_allowed = False
for subnet in const.OVERWRITE_SUBNETS:
if req.context["remote"]["addr"] in subnet:
overwrite_allowed = True
break
"""
Handle domain computer automatic enrollment
"""
machine = req.context.get("machine")
if machine:
reasons.append("machine enrollment not allowed from %s" % req.context["remote"]["addr"])
for subnet in const.MACHINE_ENROLLMENT_SUBNETS:
if req.context["remote"]["addr"] in subnet:
if common_name != machine:
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
hit = False
with DirectoryConnection() as conn:
ft = const.LDAP_COMPUTER_FILTER % ("%s$" % machine)
attribs = "cn",
r = conn.search_s(const.LDAP_BASE, 2, ft, attribs)
for dn, entry in r:
if not dn:
continue
else:
hit = True
break
if hit:
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file")
try:
mongo_doc = authority.store_request(body,address=str(req.context["remote"]["addr"]))
cert, resp.text = authority.sign(mongo_id=str(mongo_doc["_id"]),
profile="Roadwarrior", overwrite=overwrite_allowed) # TODO: handle thrown exception
logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s",
machine, dn, req.context["remote"]["addr"])
return
except errors.RequestExists:
reasons.append("same request already uploaded exists")
# We should still redirect client to long poll URL below
except errors.DuplicateCommonNameError:
logger.warning("rejected signing request with overlapping common name from %s",
req.context["remote"]["addr"])
raise falcon.HTTPConflict(
"CSR with such CN already exists",
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
else:
logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft))
"""
Process automatic signing if the IP address is whitelisted,
autosigning was requested and certificate can be automatically signed
"""
if req.get_param_as_bool("autosign"):
for subnet in const.AUTOSIGN_SUBNETS:
if req.context["remote"]["addr"] in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
mongo_doc = authority.store_request(body,address=str(req.context["remote"]["addr"]))
_, resp.text = authority.sign(mongo_id=str(mongo_doc["_id"]),
overwrite=overwrite_allowed, profile="Roadwarrior")
logger.info("Signed %s as %s is whitelisted for autosign", common_name, req.context["remote"]["addr"])
return
except EnvironmentError:
logger.info("Autosign for %s from %s failed, signed certificate already exists",
common_name, req.context["remote"]["addr"])
reasons.append("autosign failed, signed certificate already exists")
break
else:
reasons.append("IP address not whitelisted for autosign")
else:
reasons.append("autosign not requested")
# Attempt to save the request otherwise
try:
mongo_doc = authority.store_request(body,
address=str(req.context["remote"]["addr"]))
except errors.RequestExists:
reasons.append("same request already uploaded exists")
# We should still redirect client to long poll URL below
except errors.DuplicateCommonNameError:
logger.warning("rejected signing request with overlapping common name from %s",
req.context["remote"]["addr"])
raise falcon.HTTPConflict(
"CSR with such CN already exists",
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
# Wait the certificate to be signed if waiting is requested
logger.info("Signing request %s from %s put on hold, %s", common_name, req.context["remote"]["addr"], ", ".join(reasons))
if req.get_param("wait"):
header, _, der_bytes = pem.unarmor(body)
url = "https://%s/api/event/request-signed/%s" % (const.AUTHORITY_NAMESPACE, str(mongo_doc["_id"]))
click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url)
else:
# Request was accepted, but not processed
resp.status = falcon.HTTP_202
resp.text = ". ".join(reasons)
if req.client_accepts("application/json"):
resp.text = json.dumps({"title":"Accepted", "description":resp.text, "id":str(mongo_doc["_id"])},
cls=MyEncoder)
class RequestDetailResource(object):
def on_get(self, req, resp, id):
"""
Fetch certificate signing request as PEM
"""
try:
csr, csr_doc, buf = authority.get_request(id)
except errors.RequestDoesNotExist:
logger.warning("Failed to serve non-existant request %s to %s",
id, req.context["remote"]["addr"])
raise falcon.HTTPNotFound()
resp.set_header("Content-Type", "application/pkcs10")
logger.debug("Signing request %s was downloaded by %s",
csr_doc["common_name"], req.context["remote"]["addr"])
preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
if preferred_type == "application/x-pem-file":
# For certidude client, curl scripts etc
resp.set_header("Content-Type", "application/x-pem-file")
resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % csr_doc["common_name"]))
resp.text = buf
elif preferred_type == "application/json":
# For web interface events
resp.set_header("Content-Type", "application/json")
resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % csr_doc["common_name"]))
resp.text = json.dumps(dict(
submitted=csr_doc["submitted"],
common_name=csr_doc["common_name"],
id=str(csr_doc["_id"]),
address=csr_doc["user"]["request_addresss"],
md5sum=hashlib.md5(buf).hexdigest(),
sha1sum=hashlib.sha1(buf).hexdigest(),
sha256sum=hashlib.sha256(buf).hexdigest(),
sha512sum=hashlib.sha512(buf).hexdigest()), cls=MyEncoder)
else:
raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json or application/x-pem-file")
@csrf_protection
@login_required
@authorize_admin
def on_post(self, req, resp, id):
"""
Sign a certificate signing request
"""
try:
cert, buf = authority.sign(mongo_id=id,
profile=req.get_param("profile", default="Roadwarrior"),
overwrite=True,
signer=req.context.get("user").name) #if user is cached in browser then there is no name
# Mailing and long poll publishing implemented in the function above
except EnvironmentError: # no such CSR
raise falcon.HTTPNotFound(title="Not found",description="CSR not found with id %s" % id)
resp.text = "Certificate successfully signed"
resp.status = falcon.HTTP_201
resp.location = req.forwarded_uri.replace("request","sign")
cn = cert.subject.native.get("common_name")
logger.info("Signing request %s signed by %s from %s", cn,
req.context.get("user"), req.context["remote"]["addr"])
@csrf_protection
@login_required
@authorize_admin
def on_delete(self, req, resp, id):
try:
authority.delete_request(id, user=req.context.get("user"))
# Logging implemented in the function above
except errors.RequestDoesNotExist as e:
resp.text = "No certificate signing request for with id %s not found" % id
logger.warning("User %s failed to delete signing request %s from %s, reason: %s",
req.context["user"], id, req.context["remote"]["addr"], e)
raise falcon.HTTPNotFound()
except ValueError as e:
resp.text = "No ID specified %s" % id
logger.warning("User %s wanted to delete invalid signing request %s from %s, reason: %s",
req.context["user"], id, req.context["remote"]["addr"], e)
raise falcon.HTTPBadRequest()