mirror of
https://github.com/laurivosandi/certidude
synced 2025-10-30 08:59:13 +00:00
Major refactor
* Migrate to Python 3 * Update token generator mechanism * Switch to Bootstrap 4 * Switch from Iconmonstr to Font Awesome icons * Rename default CA common name to "Certidude at ca.example.lan" * Add self-enroll for the TLS server certificates * TLS client auth for lease updating * Compile assets from npm packages to /var/lib/certidude/ca.example.lan/assets
This commit is contained in:
@@ -6,7 +6,7 @@ import logging
|
||||
import os
|
||||
import click
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
from xattr import listxattr, getxattr
|
||||
from certidude import authority, mailer
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CertificateAuthorityResource(object):
|
||||
def on_get(self, req, resp):
|
||||
logger.info(u"Served CA certificate to %s", req.context.get("remote_addr"))
|
||||
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
|
||||
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
|
||||
@@ -34,23 +34,43 @@ class SessionResource(object):
|
||||
def on_get(self, req, resp):
|
||||
|
||||
def serialize_requests(g):
|
||||
for common_name, path, buf, obj, server in g():
|
||||
for common_name, path, buf, req, submitted, server in g():
|
||||
try:
|
||||
submission_address = getxattr(path, "user.request.address").decode("ascii") # TODO: move to authority.py
|
||||
except IOError:
|
||||
submission_address = None
|
||||
try:
|
||||
submission_hostname = getxattr(path, "user.request.hostname").decode("ascii") # TODO: move to authority.py
|
||||
except IOError:
|
||||
submission_hostname = None
|
||||
yield dict(
|
||||
submitted = submitted,
|
||||
common_name = common_name,
|
||||
server = server,
|
||||
address = getxattr(path, "user.request.address"), # TODO: move to authority.py
|
||||
address = submission_address,
|
||||
hostname = submission_hostname if submission_hostname != submission_address else None,
|
||||
md5sum = hashlib.md5(buf).hexdigest(),
|
||||
sha1sum = hashlib.sha1(buf).hexdigest(),
|
||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||
sha512sum = hashlib.sha512(buf).hexdigest()
|
||||
)
|
||||
|
||||
def serialize_revoked(g):
|
||||
for common_name, path, buf, cert, signed, expired, revoked in g():
|
||||
yield dict(
|
||||
serial = "%x" % cert.serial_number,
|
||||
common_name = common_name,
|
||||
# TODO: key type, key length, key exponent, key modulo
|
||||
signed = signed,
|
||||
expired = expired,
|
||||
revoked = revoked,
|
||||
sha256sum = hashlib.sha256(buf).hexdigest())
|
||||
|
||||
def serialize_certificates(g):
|
||||
for common_name, path, buf, obj, server in g():
|
||||
for common_name, path, buf, cert, signed, expires in g():
|
||||
# Extract certificate tags from filesystem
|
||||
try:
|
||||
tags = []
|
||||
for tag in getxattr(path, "user.xdg.tags").split(","):
|
||||
for tag in getxattr(path, "user.xdg.tags").decode("ascii").split(","):
|
||||
if "=" in tag:
|
||||
k, v = tag.split("=", 1)
|
||||
else:
|
||||
@@ -61,38 +81,47 @@ class SessionResource(object):
|
||||
|
||||
attributes = {}
|
||||
for key in listxattr(path):
|
||||
if key.startswith("user.machine."):
|
||||
attributes[key[13:]] = getxattr(path, key)
|
||||
if key.startswith(b"user.machine."):
|
||||
attributes[key[13:]] = getxattr(path, key).decode("ascii")
|
||||
|
||||
# Extract lease information from filesystem
|
||||
try:
|
||||
last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
lease = dict(
|
||||
inner_address = getxattr(path, "user.lease.inner_address"),
|
||||
outer_address = getxattr(path, "user.lease.outer_address"),
|
||||
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"),
|
||||
outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"),
|
||||
last_seen = last_seen,
|
||||
age = datetime.utcnow() - last_seen
|
||||
)
|
||||
except IOError: # No such attribute(s)
|
||||
lease = None
|
||||
|
||||
try:
|
||||
signer_username = getxattr(path, "user.signature.username").decode("ascii")
|
||||
except IOError:
|
||||
signer_username = None
|
||||
|
||||
yield dict(
|
||||
serial_number = "%x" % obj.serial_number,
|
||||
serial = "%x" % cert.serial_number,
|
||||
common_name = common_name,
|
||||
server = server,
|
||||
# TODO: key type, key length, key exponent, key modulo
|
||||
signed = obj["tbs_certificate"]["validity"]["not_before"].native,
|
||||
expires = obj["tbs_certificate"]["validity"]["not_after"].native,
|
||||
signed = signed,
|
||||
expires = expires,
|
||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||
signer = signer_username,
|
||||
lease = lease,
|
||||
tags = tags,
|
||||
attributes = attributes or None,
|
||||
extensions = dict([
|
||||
(e["extn_id"].native, e["extn_value"].native)
|
||||
for e in cert["tbs_certificate"]["extensions"]
|
||||
if e["extn_value"] in ("extended_key_usage",)])
|
||||
)
|
||||
|
||||
if req.context.get("user").is_admin():
|
||||
logger.info(u"Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
else:
|
||||
logger.info(u"Logged in authority user %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
logger.info("Logged in authority user %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
return dict(
|
||||
user = dict(
|
||||
name=req.context.get("user").name,
|
||||
@@ -107,7 +136,8 @@ class SessionResource(object):
|
||||
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
||||
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
|
||||
),
|
||||
common_name = authority.certificate.subject.native["common_name"],
|
||||
common_name = const.FQDN,
|
||||
title = authority.certificate.subject.native["common_name"],
|
||||
mailer = dict(
|
||||
name = config.MAILER_NAME,
|
||||
address = config.MAILER_ADDRESS
|
||||
@@ -118,16 +148,17 @@ class SessionResource(object):
|
||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||
requests=serialize_requests(authority.list_requests),
|
||||
signed=serialize_certificates(authority.list_signed),
|
||||
revoked=serialize_certificates(authority.list_revoked),
|
||||
revoked=serialize_revoked(authority.list_revoked),
|
||||
admin_users = User.objects.filter_admins(),
|
||||
user_subnets = config.USER_SUBNETS,
|
||||
autosign_subnets = config.AUTOSIGN_SUBNETS,
|
||||
request_subnets = config.REQUEST_SUBNETS,
|
||||
admin_subnets=config.ADMIN_SUBNETS,
|
||||
user_subnets = config.USER_SUBNETS or None,
|
||||
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
|
||||
request_subnets = config.REQUEST_SUBNETS or None,
|
||||
admin_subnets=config.ADMIN_SUBNETS or None,
|
||||
signature = dict(
|
||||
server_certificate_lifetime=config.SERVER_CERTIFICATE_LIFETIME,
|
||||
client_certificate_lifetime=config.CLIENT_CERTIFICATE_LIFETIME,
|
||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME
|
||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
||||
profiles = [dict(organizational_unit=ou, flags=f, lifetime=lt) for f, lt, ou in config.PROFILES.values()]
|
||||
)
|
||||
) if req.context.get("user").is_admin() else None,
|
||||
features=dict(
|
||||
@@ -155,22 +186,17 @@ class StaticResource(object):
|
||||
if content_encoding:
|
||||
resp.append_header("Content-Encoding", content_encoding)
|
||||
resp.stream = open(path, "rb")
|
||||
logger.debug(u"Serving '%s' from '%s'", req.path, path)
|
||||
logger.debug("Serving '%s' from '%s'", req.path, path)
|
||||
else:
|
||||
resp.status = falcon.HTTP_404
|
||||
resp.body = "File '%s' not found" % req.path
|
||||
logger.info(u"File '%s' not found, path resolved to '%s'", req.path, path)
|
||||
logger.info("File '%s' not found, path resolved to '%s'", req.path, path)
|
||||
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.access_route[0].decode("utf-8"))
|
||||
|
||||
def process_response(self, req, resp, resource=None):
|
||||
# wtf falcon?!
|
||||
if isinstance(resp.location, unicode):
|
||||
resp.location = resp.location.encode("ascii")
|
||||
req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0])
|
||||
|
||||
def certidude_app(log_handlers=[]):
|
||||
from certidude import config
|
||||
@@ -194,7 +220,7 @@ def certidude_app(log_handlers=[]):
|
||||
app.add_route("/api/request/", RequestListResource())
|
||||
app.add_route("/api/", SessionResource())
|
||||
|
||||
if config.BUNDLE_FORMAT and config.USER_ENROLLMENT_ALLOWED:
|
||||
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config
|
||||
app.add_route("/api/token/", TokenResource())
|
||||
|
||||
# Extended attributes for scripting etc.
|
||||
@@ -224,7 +250,6 @@ def certidude_app(log_handlers=[]):
|
||||
from .scep import SCEPResource
|
||||
app.add_route("/api/scep/", SCEPResource())
|
||||
|
||||
|
||||
# Add sink for serving static files
|
||||
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ class AttributeResource(object):
|
||||
@csrf_protection
|
||||
@whitelist_subject # TODO: sign instead
|
||||
def on_post(self, req, resp, cn):
|
||||
namespace = ("user.%s." % self.namespace).encode("ascii")
|
||||
try:
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
except IOError:
|
||||
raise falcon.HTTPNotFound()
|
||||
else:
|
||||
@@ -50,7 +51,7 @@ class AttributeResource(object):
|
||||
setxattr(path, identifier, value.encode("utf-8"))
|
||||
valid.add(identifier)
|
||||
for key in listxattr(path):
|
||||
if not key.startswith("user.%s." % self.namespace):
|
||||
if not key.startswith(namespace):
|
||||
continue
|
||||
if key not in valid:
|
||||
removexattr(path, key)
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import xattr
|
||||
from datetime import datetime
|
||||
from certidude import config, authority, push
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.auth import login_required, authorize_admin, authorize_server
|
||||
from certidude.decorators import serialize
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,9 +18,9 @@ class LeaseDetailResource(object):
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp, cn):
|
||||
try:
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
return dict(
|
||||
last_seen = xattr.getxattr(path, "user.lease.last_seen"),
|
||||
last_seen = xattr.getxattr(path, "user.lease.last_seen").decode("ascii"),
|
||||
inner_address = xattr.getxattr(path, "user.lease.inner_address").decode("ascii"),
|
||||
outer_address = xattr.getxattr(path, "user.lease.outer_address").decode("ascii")
|
||||
)
|
||||
@@ -29,10 +29,10 @@ class LeaseDetailResource(object):
|
||||
|
||||
|
||||
class LeaseResource(object):
|
||||
@authorize_server
|
||||
def on_post(self, req, resp):
|
||||
# TODO: verify signature
|
||||
common_name = req.get_param("client", required=True)
|
||||
path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions
|
||||
path, buf, cert, signed, expires = authority.get_signed(common_name) # TODO: catch exceptions
|
||||
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
|
||||
raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied")
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class OCSPResource(object):
|
||||
else:
|
||||
raise falcon.HTTPMethodNotAllowed()
|
||||
|
||||
fh = open(config.AUTHORITY_CERTIFICATE_PATH)
|
||||
fh = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") # TODO: import from authority
|
||||
server_certificate = asymmetric.load_certificate(fh.read())
|
||||
fh.close()
|
||||
|
||||
@@ -35,7 +35,7 @@ class OCSPResource(object):
|
||||
if ext["extn_id"].native == "nonce":
|
||||
response_extensions.append(
|
||||
ocsp.ResponseDataExtension({
|
||||
'extn_id': u"nonce",
|
||||
'extn_id': "nonce",
|
||||
'critical': False,
|
||||
'extn_value': ext["extn_value"]
|
||||
})
|
||||
@@ -51,18 +51,19 @@ class OCSPResource(object):
|
||||
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial))
|
||||
assert link_target.startswith("../")
|
||||
assert link_target.endswith(".pem")
|
||||
path, buf, cert = authority.get_signed(link_target[3:-4])
|
||||
path, buf, cert, signed, expires = authority.get_signed(link_target[3:-4])
|
||||
if serial != cert.serial_number:
|
||||
raise EnvironmentError("integrity check failed")
|
||||
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %x" % (link_target, cert.serial_number))
|
||||
raise EnvironmentError("Integrity check failed")
|
||||
status = ocsp.CertStatus(name='good', value=None)
|
||||
except EnvironmentError:
|
||||
try:
|
||||
path, buf, cert, revoked = authority.get_revoked(serial)
|
||||
path, buf, cert, signed, expires, revoked = authority.get_revoked(serial)
|
||||
status = ocsp.CertStatus(
|
||||
name='revoked',
|
||||
value={
|
||||
'revocation_time': revoked,
|
||||
'revocation_reason': u"key_compromise",
|
||||
'revocation_reason': "key_compromise",
|
||||
})
|
||||
except EnvironmentError:
|
||||
status = ocsp.CertStatus(name="unknown", value=None)
|
||||
@@ -70,7 +71,7 @@ class OCSPResource(object):
|
||||
responses.append({
|
||||
'cert_id': {
|
||||
'hash_algorithm': {
|
||||
'algorithm': u"sha1"
|
||||
'algorithm': "sha1"
|
||||
},
|
||||
'issuer_name_hash': server_certificate.asn1.subject.sha1,
|
||||
'issuer_key_hash': server_certificate.public_key.asn1.sha1,
|
||||
@@ -89,13 +90,13 @@ class OCSPResource(object):
|
||||
})
|
||||
|
||||
resp.body = ocsp.OCSPResponse({
|
||||
'response_status': u"successful",
|
||||
'response_status': "successful",
|
||||
'response_bytes': {
|
||||
'response_type': u"basic_ocsp_response",
|
||||
'response_type': "basic_ocsp_response",
|
||||
'response': {
|
||||
'tbs_response_data': response_data,
|
||||
'certs': [server_certificate.asn1],
|
||||
'signature_algorithm': {'algorithm': u"sha1_rsa"},
|
||||
'signature_algorithm': {'algorithm': "sha1_rsa"},
|
||||
'signature': asymmetric.rsa_pkcs1v15_sign(
|
||||
authority.private_key,
|
||||
response_data.dump(),
|
||||
|
||||
@@ -11,7 +11,7 @@ from asn1crypto.csr import CertificationRequest
|
||||
from base64 import b64decode
|
||||
from certidude import config, authority, push, errors
|
||||
from certidude.auth import login_required, login_optional, authorize_admin
|
||||
from certidude.decorators import serialize, csrf_protection
|
||||
from certidude.decorators import csrf_protection, MyEncoder
|
||||
from certidude.firewall import whitelist_subnets, whitelist_content_types
|
||||
from datetime import datetime
|
||||
from oscrypto import asymmetric
|
||||
@@ -36,7 +36,7 @@ class RequestListResource(object):
|
||||
Validate and parse certificate signing request, the RESTful way
|
||||
"""
|
||||
reasons = []
|
||||
body = req.stream.read(req.content_length).encode("ascii")
|
||||
body = req.stream.read(req.content_length)
|
||||
|
||||
header, _, der_bytes = pem.unarmor(body)
|
||||
csr = CertificationRequest.load(der_bytes)
|
||||
@@ -56,7 +56,7 @@ class RequestListResource(object):
|
||||
# Automatic enroll with Kerberos machine cerdentials
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
cert, resp.body = authority._sign(csr, body, overwrite=True)
|
||||
logger.info(u"Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||
machine, req.context.get("remote_addr"))
|
||||
return
|
||||
else:
|
||||
@@ -66,7 +66,7 @@ class RequestListResource(object):
|
||||
Attempt to renew certificate using currently valid key pair
|
||||
"""
|
||||
try:
|
||||
path, buf, cert = authority.get_signed(common_name)
|
||||
path, buf, cert, signed, expires = authority.get_signed(common_name)
|
||||
except EnvironmentError:
|
||||
pass # No currently valid certificate for this common name
|
||||
else:
|
||||
@@ -85,8 +85,8 @@ class RequestListResource(object):
|
||||
|
||||
try:
|
||||
renewal_signature = b64decode(renewal_header)
|
||||
except TypeError, ValueError:
|
||||
logger.error(u"Renewal failed, bad signature supplied for %s", common_name)
|
||||
except (TypeError, ValueError):
|
||||
logger.error("Renewal failed, bad signature supplied for %s", common_name)
|
||||
reasons.append("Renewal failed, bad signature supplied")
|
||||
else:
|
||||
try:
|
||||
@@ -94,20 +94,20 @@ class RequestListResource(object):
|
||||
asymmetric.load_certificate(cert),
|
||||
renewal_signature, buf + body, "sha512")
|
||||
except SignatureError:
|
||||
logger.error(u"Renewal failed, invalid signature supplied for %s", common_name)
|
||||
logger.error("Renewal failed, invalid signature supplied for %s", common_name)
|
||||
reasons.append("Renewal failed, invalid signature supplied")
|
||||
else:
|
||||
# At this point renewal signature was valid but we need to perform some extra checks
|
||||
if datetime.utcnow() > expires:
|
||||
logger.error(u"Renewal failed, current certificate for %s has expired", common_name)
|
||||
logger.error("Renewal failed, current certificate for %s has expired", common_name)
|
||||
reasons.append("Renewal failed, current certificate expired")
|
||||
elif not config.CERTIFICATE_RENEWAL_ALLOWED:
|
||||
logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name)
|
||||
logger.error("Renewal requested for %s, but not allowed by authority settings", common_name)
|
||||
reasons.append("Renewal requested, but not allowed by authority settings")
|
||||
else:
|
||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||
_, resp.body = authority._sign(csr, body, overwrite=True)
|
||||
logger.info(u"Renewed certificate for %s", common_name)
|
||||
logger.info("Renewed certificate for %s", common_name)
|
||||
return
|
||||
|
||||
|
||||
@@ -122,10 +122,10 @@ class RequestListResource(object):
|
||||
try:
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
_, resp.body = authority._sign(csr, body)
|
||||
logger.info(u"Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||
return
|
||||
except EnvironmentError:
|
||||
logger.info(u"Autosign for %s from %s failed, signed certificate already exists",
|
||||
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
||||
common_name, req.context.get("remote_addr"))
|
||||
reasons.append("Autosign failed, signed certificate already exists")
|
||||
break
|
||||
@@ -143,7 +143,7 @@ class RequestListResource(object):
|
||||
# We should still redirect client to long poll URL below
|
||||
except errors.DuplicateCommonNameError:
|
||||
# TODO: Certificate renewal
|
||||
logger.warning(u"Rejected signing request with overlapping common name from %s",
|
||||
logger.warning("Rejected signing request with overlapping common name from %s",
|
||||
req.context.get("remote_addr"))
|
||||
raise falcon.HTTPConflict(
|
||||
"CSR with such CN already exists",
|
||||
@@ -152,14 +152,14 @@ class RequestListResource(object):
|
||||
push.publish("request-submitted", common_name)
|
||||
|
||||
# Wait the certificate to be signed if waiting is requested
|
||||
logger.info(u"Stored signing request %s from %s", common_name, req.context.get("remote_addr"))
|
||||
logger.info("Stored signing request %s from %s", common_name, req.context.get("remote_addr"))
|
||||
if req.get_param("wait"):
|
||||
# Redirect to nginx pub/sub
|
||||
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
||||
click.echo("Redirecting to: %s" % url)
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.set_header("Location", url.encode("ascii"))
|
||||
logger.debug(u"Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
|
||||
resp.set_header("Location", url)
|
||||
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
|
||||
else:
|
||||
# Request was accepted, but not processed
|
||||
resp.status = falcon.HTTP_202
|
||||
@@ -173,14 +173,14 @@ class RequestDetailResource(object):
|
||||
"""
|
||||
|
||||
try:
|
||||
path, buf, _ = authority.get_request(cn)
|
||||
path, buf, _, submitted = authority.get_request(cn)
|
||||
except errors.RequestDoesNotExist:
|
||||
logger.warning(u"Failed to serve non-existant request %s to %s",
|
||||
logger.warning("Failed to serve non-existant request %s to %s",
|
||||
cn, req.context.get("remote_addr"))
|
||||
raise falcon.HTTPNotFound()
|
||||
|
||||
resp.set_header("Content-Type", "application/pkcs10")
|
||||
logger.debug(u"Signing request %s was downloaded by %s",
|
||||
logger.debug("Signing request %s was downloaded by %s",
|
||||
cn, req.context.get("remote_addr"))
|
||||
|
||||
preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
|
||||
@@ -195,13 +195,14 @@ class RequestDetailResource(object):
|
||||
resp.set_header("Content-Type", "application/json")
|
||||
resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn))
|
||||
resp.body = json.dumps(dict(
|
||||
submitted = submitted,
|
||||
common_name = cn,
|
||||
server = authority.server_flags(cn),
|
||||
address = getxattr(path, "user.request.address"), # TODO: move to authority.py
|
||||
address = getxattr(path, "user.request.address").decode("ascii"), # TODO: move to authority.py
|
||||
md5sum = hashlib.md5(buf).hexdigest(),
|
||||
sha1sum = hashlib.sha1(buf).hexdigest(),
|
||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||
sha512sum = hashlib.sha512(buf).hexdigest()))
|
||||
sha512sum = hashlib.sha512(buf).hexdigest()), cls=MyEncoder)
|
||||
else:
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"Client did not accept application/json or application/x-pem-file")
|
||||
@@ -214,13 +215,16 @@ class RequestDetailResource(object):
|
||||
"""
|
||||
Sign a certificate signing request
|
||||
"""
|
||||
cert, buf = authority.sign(cn, overwrite=True)
|
||||
# Mailing and long poll publishing implemented in the function above
|
||||
try:
|
||||
cert, buf = authority.sign(cn, ou=req.get_param("ou"), overwrite=True, signer=req.context.get("user").name)
|
||||
# Mailing and long poll publishing implemented in the function above
|
||||
except EnvironmentError: # no such CSR
|
||||
raise falcon.HTTPNotFound()
|
||||
|
||||
resp.body = "Certificate successfully signed"
|
||||
resp.status = falcon.HTTP_201
|
||||
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
||||
logger.info(u"Signing request %s signed by %s from %s", cn,
|
||||
logger.info("Signing request %s signed by %s from %s", cn,
|
||||
req.context.get("user"), req.context.get("remote_addr"))
|
||||
|
||||
@csrf_protection
|
||||
@@ -232,6 +236,6 @@ class RequestDetailResource(object):
|
||||
# Logging implemented in the function above
|
||||
except errors.RequestDoesNotExist as e:
|
||||
resp.body = "No certificate signing request for %s found" % cn
|
||||
logger.warning(u"User %s failed to delete signing request %s from %s, reason: %s",
|
||||
logger.warning("User %s failed to delete signing request %s from %s, reason: %s",
|
||||
req.context["user"], cn, req.context.get("remote_addr"), e)
|
||||
raise falcon.HTTPNotFound()
|
||||
|
||||
@@ -18,25 +18,25 @@ class RevocationListResource(object):
|
||||
resp.set_header("Content-Type", "application/x-pkcs7-crl")
|
||||
resp.append_header(
|
||||
"Content-Disposition",
|
||||
("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii"))
|
||||
("attachment; filename=%s.crl" % const.HOSTNAME))
|
||||
# Convert PEM to DER
|
||||
logger.debug(u"Serving revocation list (DER) to %s", req.context.get("remote_addr"))
|
||||
logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr"))
|
||||
resp.body = export_crl(pem=False)
|
||||
elif req.client_accepts("application/x-pem-file"):
|
||||
if req.get_param_as_bool("wait"):
|
||||
url = config.LONG_POLL_SUBSCRIBE % "crl"
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.set_header("Location", url.encode("ascii"))
|
||||
logger.debug(u"Redirecting to CRL request to %s", url)
|
||||
resp.set_header("Location", url)
|
||||
logger.debug("Redirecting to CRL request to %s", url)
|
||||
resp.body = "Redirecting to %s" % url
|
||||
else:
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
resp.append_header(
|
||||
"Content-Disposition",
|
||||
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
|
||||
logger.debug(u"Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
|
||||
("attachment; filename=%s-crl.pem" % const.HOSTNAME))
|
||||
logger.debug("Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
|
||||
resp.body = export_crl()
|
||||
else:
|
||||
logger.debug(u"Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))
|
||||
logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"Client did not accept application/x-pkcs7-crl or application/x-pem-file")
|
||||
|
||||
@@ -15,12 +15,12 @@ from oscrypto.errors import SignatureError
|
||||
class SetOfPrintableString(SetOf):
|
||||
_child_spec = PrintableString
|
||||
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.2'] = u"message_type"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.3'] = u"pki_status"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.4'] = u"fail_info"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.5'] = u"sender_nonce"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.6'] = u"recipient_nonce"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.7'] = u"trans_id"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.2'] = "message_type"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.3'] = "pki_status"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.4'] = "fail_info"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.5'] = "sender_nonce"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.6'] = "recipient_nonce"
|
||||
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.7'] = "trans_id"
|
||||
|
||||
cms.CMSAttribute._oid_specs['message_type'] = SetOfPrintableString
|
||||
cms.CMSAttribute._oid_specs['pki_status'] = SetOfPrintableString
|
||||
@@ -41,25 +41,20 @@ class SCEPResource(object):
|
||||
def on_get(self, req, resp):
|
||||
operation = req.get_param("operation")
|
||||
if operation.lower() == "getcacert":
|
||||
resp.stream = keys.parse_certificate(authority.certificate_buf).dump()
|
||||
resp.body = keys.parse_certificate(authority.certificate_buf).dump()
|
||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||
return
|
||||
|
||||
# Parse CA certificate
|
||||
fh = open(config.AUTHORITY_CERTIFICATE_PATH)
|
||||
server_certificate = asymmetric.load_certificate(fh.read())
|
||||
fh.close()
|
||||
|
||||
# If we bump into exceptions later
|
||||
encrypted_container = b""
|
||||
attr_list = [
|
||||
cms.CMSAttribute({
|
||||
'type': u"message_type",
|
||||
'values': [u"3"]
|
||||
'type': "message_type",
|
||||
'values': ["3"]
|
||||
}),
|
||||
cms.CMSAttribute({
|
||||
'type': u"pki_status",
|
||||
'values': [u"2"] # rejected
|
||||
'type': "pki_status",
|
||||
'values': ["2"] # rejected
|
||||
})
|
||||
]
|
||||
|
||||
@@ -98,7 +93,7 @@ class SCEPResource(object):
|
||||
|
||||
assert message_digest
|
||||
msg = signer["signed_attrs"].dump(force=True)
|
||||
assert msg[0] == b"\xa0", repr(msg[0])
|
||||
assert msg[0] == 160
|
||||
|
||||
# Verify signature
|
||||
try:
|
||||
@@ -139,9 +134,9 @@ class SCEPResource(object):
|
||||
signed_certificate = asymmetric.load_certificate(buf)
|
||||
content = signed_certificate.asn1.dump()
|
||||
|
||||
except SCEPError, e:
|
||||
except SCEPError as e:
|
||||
attr_list.append(cms.CMSAttribute({
|
||||
'type': u"fail_info",
|
||||
'type': "fail_info",
|
||||
'values': ["%d" % e.code]
|
||||
}))
|
||||
else:
|
||||
@@ -151,17 +146,17 @@ class SCEPResource(object):
|
||||
##################################
|
||||
|
||||
degenerate = cms.ContentInfo({
|
||||
'content_type': u"signed_data",
|
||||
'content_type': "signed_data",
|
||||
'content': cms.SignedData({
|
||||
'version': u"v1",
|
||||
'version': "v1",
|
||||
'certificates': [signed_certificate.asn1],
|
||||
'digest_algorithms': [cms.DigestAlgorithm({
|
||||
'algorithm': u"md5"
|
||||
'algorithm': "md5"
|
||||
})],
|
||||
'encap_content_info': {
|
||||
'content_type': u"data",
|
||||
'content_type': "data",
|
||||
'content': cms.ContentInfo({
|
||||
'content_type': u"signed_data",
|
||||
'content_type': "signed_data",
|
||||
'content': None
|
||||
}).dump()
|
||||
},
|
||||
@@ -180,7 +175,7 @@ class SCEPResource(object):
|
||||
|
||||
ri = cms.RecipientInfo({
|
||||
'ktri': cms.KeyTransRecipientInfo({
|
||||
'version': u"v0",
|
||||
'version': "v0",
|
||||
'rid': cms.RecipientIdentifier({
|
||||
'issuer_and_serial_number': cms.IssuerAndSerialNumber({
|
||||
'issuer': current_certificate.chosen["tbs_certificate"]["issuer"],
|
||||
@@ -188,7 +183,7 @@ class SCEPResource(object):
|
||||
}),
|
||||
}),
|
||||
'key_encryption_algorithm': {
|
||||
'algorithm': u"rsa"
|
||||
'algorithm': "rsa"
|
||||
},
|
||||
'encrypted_key': asymmetric.rsa_pkcs1v15_encrypt(
|
||||
asymmetric.load_certificate(current_certificate.chosen.dump()), key)
|
||||
@@ -196,14 +191,14 @@ class SCEPResource(object):
|
||||
})
|
||||
|
||||
encrypted_container = cms.ContentInfo({
|
||||
'content_type': u"enveloped_data",
|
||||
'content_type': "enveloped_data",
|
||||
'content': cms.EnvelopedData({
|
||||
'version': u"v1",
|
||||
'version': "v1",
|
||||
'recipient_infos': [ri],
|
||||
'encrypted_content_info': {
|
||||
'content_type': u"data",
|
||||
'content_type': "data",
|
||||
'content_encryption_algorithm': {
|
||||
'algorithm': u"des",
|
||||
'algorithm': "des",
|
||||
'parameters': iv
|
||||
},
|
||||
'encrypted_content': encrypted_content
|
||||
@@ -213,16 +208,16 @@ class SCEPResource(object):
|
||||
|
||||
attr_list = [
|
||||
cms.CMSAttribute({
|
||||
'type': u"message_digest",
|
||||
'type': "message_digest",
|
||||
'values': [hashlib.sha1(encrypted_container).digest()]
|
||||
}),
|
||||
cms.CMSAttribute({
|
||||
'type': u"message_type",
|
||||
'values': [u"3"]
|
||||
'type': "message_type",
|
||||
'values': ["3"]
|
||||
}),
|
||||
cms.CMSAttribute({
|
||||
'type': u"pki_status",
|
||||
'values': [u"0"] # ok
|
||||
'type': "pki_status",
|
||||
'values': ["0"] # ok
|
||||
})
|
||||
]
|
||||
finally:
|
||||
@@ -233,26 +228,26 @@ class SCEPResource(object):
|
||||
|
||||
attrs = cms.CMSAttributes(attr_list + [
|
||||
cms.CMSAttribute({
|
||||
'type': u"recipient_nonce",
|
||||
'type': "recipient_nonce",
|
||||
'values': [sender_nonce]
|
||||
}),
|
||||
cms.CMSAttribute({
|
||||
'type': u"trans_id",
|
||||
'type': "trans_id",
|
||||
'values': [transaction_id]
|
||||
})
|
||||
])
|
||||
|
||||
signer = cms.SignerInfo({
|
||||
"signed_attrs": attrs,
|
||||
'version': u"v1",
|
||||
'version': "v1",
|
||||
'sid': cms.SignerIdentifier({
|
||||
'issuer_and_serial_number': cms.IssuerAndSerialNumber({
|
||||
'issuer': server_certificate.asn1["tbs_certificate"]["issuer"],
|
||||
'serial_number': server_certificate.asn1["tbs_certificate"]["serial_number"],
|
||||
'issuer': authority.certificate.issuer,
|
||||
'serial_number': authority.certificate.serial_number,
|
||||
}),
|
||||
}),
|
||||
'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}),
|
||||
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': u"rsassa_pkcs1v15"}),
|
||||
'digest_algorithm': algos.DigestAlgorithm({'algorithm': "sha1"}),
|
||||
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
|
||||
'signature': asymmetric.rsa_pkcs1v15_sign(
|
||||
authority.private_key,
|
||||
b"\x31" + attrs.dump()[1:],
|
||||
@@ -262,15 +257,15 @@ class SCEPResource(object):
|
||||
|
||||
resp.append_header("Content-Type", "application/x-pki-message")
|
||||
resp.body = cms.ContentInfo({
|
||||
'content_type': u"signed_data",
|
||||
'content_type': "signed_data",
|
||||
'content': cms.SignedData({
|
||||
'version': u"v1",
|
||||
'certificates': [x509.Certificate.load(server_certificate.asn1.dump())], # wat
|
||||
'version': "v1",
|
||||
'certificates': [authority.certificate],
|
||||
'digest_algorithms': [cms.DigestAlgorithm({
|
||||
'algorithm': u"sha1"
|
||||
'algorithm': "sha1"
|
||||
})],
|
||||
'encap_content_info': {
|
||||
'content_type': u"data",
|
||||
'content_type': "data",
|
||||
'content': encrypted_container
|
||||
},
|
||||
'signer_infos': [signer]
|
||||
|
||||
@@ -22,7 +22,7 @@ class ScriptResource():
|
||||
k, v = tag.split("=", 1)
|
||||
named_tags[k] = v
|
||||
else:
|
||||
other_tags.append(v)
|
||||
other_tags.append(tag)
|
||||
except AttributeError: # No tags
|
||||
pass
|
||||
|
||||
@@ -34,5 +34,5 @@ class ScriptResource():
|
||||
other_tags=other_tags,
|
||||
named_tags=named_tags,
|
||||
attributes=attribs.get("user").get("machine"))
|
||||
logger.info(u"Served script %s for %s at %s" % (script, cn, req.context["remote_addr"]))
|
||||
logger.info("Served script %s for %s at %s" % (script, cn, req.context["remote_addr"]))
|
||||
# TODO: Assert time is within reasonable range
|
||||
|
||||
@@ -6,6 +6,7 @@ import hashlib
|
||||
from certidude import authority
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import csrf_protection
|
||||
from xattr import getxattr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,9 +15,9 @@ class SignedCertificateDetailResource(object):
|
||||
|
||||
preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
|
||||
try:
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
except EnvironmentError:
|
||||
logger.warning(u"Failed to serve non-existant certificate %s to %s",
|
||||
logger.warning("Failed to serve non-existant certificate %s to %s",
|
||||
cn, req.context.get("remote_addr"))
|
||||
raise falcon.HTTPNotFound()
|
||||
|
||||
@@ -24,21 +25,26 @@ class SignedCertificateDetailResource(object):
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % cn))
|
||||
resp.body = buf
|
||||
logger.debug(u"Served certificate %s to %s as application/x-pem-file",
|
||||
logger.debug("Served certificate %s to %s as application/x-pem-file",
|
||||
cn, req.context.get("remote_addr"))
|
||||
elif preferred_type == "application/json":
|
||||
resp.set_header("Content-Type", "application/json")
|
||||
resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn))
|
||||
try:
|
||||
signer_username = getxattr(path, "user.signature.username").decode("ascii")
|
||||
except IOError:
|
||||
signer_username = None
|
||||
resp.body = json.dumps(dict(
|
||||
common_name = cn,
|
||||
signer = signer_username,
|
||||
serial_number = "%x" % cert.serial_number,
|
||||
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
sha256sum = hashlib.sha256(buf).hexdigest()))
|
||||
logger.debug(u"Served certificate %s to %s as application/json",
|
||||
logger.debug("Served certificate %s to %s as application/json",
|
||||
cn, req.context.get("remote_addr"))
|
||||
else:
|
||||
logger.debug(u"Client did not accept application/json or application/x-pem-file")
|
||||
logger.debug("Client did not accept application/json or application/x-pem-file")
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"Client did not accept application/json or application/x-pem-file")
|
||||
|
||||
@@ -46,7 +52,7 @@ class SignedCertificateDetailResource(object):
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_delete(self, req, resp, cn):
|
||||
logger.info(u"Revoked certificate %s by %s from %s",
|
||||
logger.info("Revoked certificate %s by %s from %s",
|
||||
cn, req.context.get("user"), req.context.get("remote_addr"))
|
||||
authority.revoke(cn)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ class TagResource(object):
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp, cn):
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
tags = []
|
||||
try:
|
||||
for tag in getxattr(path, "user.xdg.tags").split(","):
|
||||
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
|
||||
if "=" in tag:
|
||||
k, v = tag.split("=", 1)
|
||||
else:
|
||||
@@ -30,7 +30,7 @@ class TagResource(object):
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_post(self, req, resp, cn):
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
key, value = req.get_param("key", required=True), req.get_param("value", required=True)
|
||||
try:
|
||||
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
|
||||
@@ -41,7 +41,7 @@ class TagResource(object):
|
||||
else:
|
||||
tags.add("%s=%s" % (key,value))
|
||||
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
||||
logger.debug(u"Tag %s=%s set for %s" % (key, value, cn))
|
||||
logger.debug("Tag %s=%s set for %s" % (key, value, cn))
|
||||
push.publish("tag-update", cn)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class TagDetailResource(object):
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_put(self, req, resp, cn, tag):
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
value = req.get_param("value", required=True)
|
||||
try:
|
||||
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
|
||||
@@ -65,19 +65,19 @@ class TagDetailResource(object):
|
||||
else:
|
||||
tags.add(value)
|
||||
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
||||
logger.debug(u"Tag %s set to %s for %s" % (tag, value, cn))
|
||||
logger.debug("Tag %s set to %s for %s" % (tag, value, cn))
|
||||
push.publish("tag-update", cn)
|
||||
|
||||
@csrf_protection
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_delete(self, req, resp, cn, tag):
|
||||
path, buf, cert = authority.get_signed(cn)
|
||||
tags = set(getxattr(path, "user.xdg.tags").split(","))
|
||||
path, buf, cert, signed, expires = authority.get_signed(cn)
|
||||
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
|
||||
tags.remove(tag)
|
||||
if not tags:
|
||||
removexattr(path, "user.xdg.tags")
|
||||
else:
|
||||
setxattr(path, "user.xdg.tags", ",".join(tags))
|
||||
logger.debug(u"Tag %s removed for %s" % (tag, cn))
|
||||
logger.debug("Tag %s removed for %s" % (tag, cn))
|
||||
push.publish("tag-update", cn)
|
||||
|
||||
@@ -4,27 +4,20 @@ import logging
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
from asn1crypto import pem
|
||||
from asn1crypto.csr import CertificationRequest
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from certidude import mailer
|
||||
from certidude.decorators import serialize
|
||||
from certidude.user import User
|
||||
from certidude import config, authority
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KEYWORDS = (
|
||||
(u"Android", u"android"),
|
||||
(u"iPhone", u"iphone"),
|
||||
(u"iPad", u"ipad"),
|
||||
(u"Ubuntu", u"ubuntu"),
|
||||
(u"Fedora", u"fedora"),
|
||||
(u"Linux", u"linux"),
|
||||
(u"Macintosh", u"mac"),
|
||||
)
|
||||
|
||||
class TokenResource(object):
|
||||
def on_get(self, req, resp):
|
||||
def on_put(self, req, resp):
|
||||
# Consume token
|
||||
now = time()
|
||||
timestamp = req.get_param_as_int("t", required=True)
|
||||
@@ -32,8 +25,8 @@ class TokenResource(object):
|
||||
user = User.objects.get(username)
|
||||
csum = hashlib.sha256()
|
||||
csum.update(config.TOKEN_SECRET)
|
||||
csum.update(username)
|
||||
csum.update(str(timestamp))
|
||||
csum.update(username.encode("ascii"))
|
||||
csum.update(str(timestamp).encode("ascii"))
|
||||
|
||||
margin = 300 # Tolerate 5 minute clock skew as Kerberos does
|
||||
if csum.hexdigest() != req.get_param("c", required=True):
|
||||
@@ -44,46 +37,46 @@ class TokenResource(object):
|
||||
raise falcon.HTTPForbidden("Forbidden", "Token expired")
|
||||
|
||||
# At this point consider token to be legitimate
|
||||
|
||||
common_name = username
|
||||
if config.USER_MULTIPLE_CERTIFICATES:
|
||||
for key, value in KEYWORDS:
|
||||
if key in req.user_agent:
|
||||
device_identifier = value
|
||||
break
|
||||
else:
|
||||
device_identifier = u"unknown-device"
|
||||
common_name = u"%s@%s-%s" % (common_name, device_identifier, \
|
||||
hashlib.sha256(req.user_agent).hexdigest()[:8])
|
||||
|
||||
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
|
||||
if config.BUNDLE_FORMAT == "p12":
|
||||
resp.set_header("Content-Type", "application/x-pkcs12")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
elif config.BUNDLE_FORMAT == "ovpn":
|
||||
resp.set_header("Content-Type", "application/x-openvpn")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_ovpn_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
else:
|
||||
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)
|
||||
body = req.stream.read(req.content_length)
|
||||
header, _, der_bytes = pem.unarmor(body)
|
||||
csr = CertificationRequest.load(der_bytes)
|
||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
|
||||
try:
|
||||
_, resp.body = authority._sign(csr, body)
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
logger.info("Autosigned %s as proven by token ownership", common_name)
|
||||
except FileExistsError:
|
||||
logger.info("Won't autosign duplicate %s", common_name)
|
||||
raise falcon.HTTPConflict(
|
||||
"Certificate with such common name (CN) already exists",
|
||||
"Will not overwrite existing certificate signing request, explicitly delete existing one and try again")
|
||||
|
||||
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_post(self, req, resp):
|
||||
# Generate token
|
||||
issuer = req.context.get("user")
|
||||
username = req.get_param("user", required=True)
|
||||
user = User.objects.get(username)
|
||||
username = req.get_param("username")
|
||||
secondary = req.get_param("mail")
|
||||
|
||||
if username:
|
||||
# Otherwise try to look up user so we can derive their e-mail address
|
||||
user = User.objects.get(username)
|
||||
else:
|
||||
# If no username is specified, assume it's intended for someone outside domain
|
||||
username = "guest-%s" % hashlib.sha256(secondary.encode("ascii")).hexdigest()[-8:]
|
||||
if not secondary:
|
||||
raise
|
||||
|
||||
timestamp = int(time())
|
||||
csum = hashlib.sha256()
|
||||
csum.update(config.TOKEN_SECRET)
|
||||
csum.update(username)
|
||||
csum.update(str(timestamp))
|
||||
args = "u=%s&t=%d&c=%s" % (username, timestamp, csum.hexdigest())
|
||||
csum.update(username.encode("ascii"))
|
||||
csum.update(str(timestamp).encode("ascii"))
|
||||
args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name)
|
||||
|
||||
# Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata
|
||||
token_created = datetime.fromtimestamp(timestamp)
|
||||
@@ -93,7 +86,11 @@ class TokenResource(object):
|
||||
token_timezone = fh.read().strip()
|
||||
except EnvironmentError:
|
||||
token_timezone = None
|
||||
url = "%s#%s" % (config.TOKEN_URL, args)
|
||||
context = globals()
|
||||
context.update(locals())
|
||||
mailer.send("token.md", to=user, **context)
|
||||
resp.body = args
|
||||
return {
|
||||
"token": args,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user