1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Merge pull request #40 from plaes/authority-rework

Authority refactor
This commit is contained in:
Lauri Võsandi 2018-02-03 17:13:44 +02:00 committed by GitHub
commit a1f7b5fca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 127 additions and 144 deletions

View File

@ -9,9 +9,9 @@ script:
- sudo apt install software-properties-common python3-setuptools python3-mysql.connector python3-pyxattr
- sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04
- sudo easy_install3 pip
- sudo pip3 install -r requirements.txt
- sudo pip3 install codecov pytest-cov requests-kerberos
- sudo pip3 install -e .
- sudo -H pip3 install -r requirements.txt
- sudo -H pip3 install codecov pytest-cov requests-kerberos
- sudo -H pip3 install -e .
- echo ca | sudo tee /etc/hostname
- echo 127.0.0.1 localhost | sudo tee /etc/hosts
- echo 127.0.1.1 ca.example.lan ca | sudo tee -a /etc/hosts
@ -21,6 +21,4 @@ script:
- sudo coverage combine
- sudo coverage report
- sudo coverage xml -i
cache:
directories:
- $HOME/.cache/pip
cache: pip

View File

@ -4,16 +4,14 @@ import falcon
import mimetypes
import logging
import os
import click
import hashlib
from datetime import datetime, timedelta
from time import sleep
from datetime import datetime
from xattr import listxattr, getxattr
from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin
from certidude.auth import login_required
from certidude.user import User
from certidude.decorators import serialize, csrf_protection
from certidude import const, config
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
@ -27,7 +25,7 @@ class CertificateAuthorityResource(object):
const.HOSTNAME.encode("ascii"))
class SessionResource(object):
class SessionResource(AuthorityHandler):
@csrf_protection
@serialize
@login_required
@ -44,7 +42,7 @@ class SessionResource(object):
except IOError:
submission_hostname = None
yield dict(
server = authority.server_flags(common_name),
server = self.authority.server_flags(common_name),
submitted = submitted,
common_name = common_name,
address = submission_address,
@ -142,7 +140,7 @@ class SessionResource(object):
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
),
common_name = const.FQDN,
title = authority.certificate.subject.native["common_name"],
title = self.authority.certificate.subject.native["common_name"],
mailer = dict(
name = config.MAILER_NAME,
address = config.MAILER_ADDRESS
@ -151,9 +149,9 @@ class SessionResource(object):
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
requests=serialize_requests(authority.list_requests),
signed=serialize_certificates(authority.list_signed),
revoked=serialize_revoked(authority.list_revoked),
requests=serialize_requests(self.authority.list_requests),
signed=serialize_certificates(self.authority.list_signed),
revoked=serialize_revoked(self.authority.list_revoked),
admin_users = User.objects.filter_admins(),
user_subnets = config.USER_SUBNETS or None,
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
@ -202,7 +200,7 @@ class NormalizeMiddleware(object):
req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0])
def certidude_app(log_handlers=[]):
from certidude import config
from certidude import authority, config
from .signed import SignedCertificateDetailResource
from .request import RequestListResource, RequestDetailResource
from .lease import LeaseResource, LeaseDetailResource
@ -219,30 +217,30 @@ def certidude_app(log_handlers=[]):
# Certificate authority API calls
app.add_route("/api/certificate/", CertificateAuthorityResource())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
app.add_route("/api/request/{cn}/", RequestDetailResource())
app.add_route("/api/request/", RequestListResource())
app.add_route("/api/", SessionResource())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority))
app.add_route("/api/request/{cn}/", RequestDetailResource(authority))
app.add_route("/api/request/", RequestListResource(authority))
app.add_route("/api/", SessionResource(authority))
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config
app.add_route("/api/token/", TokenResource())
app.add_route("/api/token/", TokenResource(authority))
# Extended attributes for scripting etc.
app.add_route("/api/signed/{cn}/attr/", AttributeResource(namespace="machine"))
app.add_route("/api/signed/{cn}/script/", ScriptResource())
app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))
app.add_route("/api/signed/{cn}/script/", ScriptResource(authority))
# API calls used by pushed events on the JS end
app.add_route("/api/signed/{cn}/tag/", TagResource())
app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource())
app.add_route("/api/signed/{cn}/tag/", TagResource(authority))
app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource(authority))
# API call used to delete existing tags
app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource())
app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource(authority))
# Gateways can submit leases via this API call
app.add_route("/api/lease/", LeaseResource())
app.add_route("/api/lease/", LeaseResource(authority))
# Bootstrap resource
app.add_route("/api/bootstrap/", BootstrapResource())
app.add_route("/api/bootstrap/", BootstrapResource(authority))
# LEDE image builder resource
app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource())
@ -250,19 +248,19 @@ def certidude_app(log_handlers=[]):
# Add CRL handler if we have any whitelisted subnets
if config.CRL_SUBNETS:
from .revoked import RevocationListResource
app.add_route("/api/revoked/", RevocationListResource())
app.add_route("/api/revoked/", RevocationListResource(authority))
# Add SCEP handler if we have any whitelisted subnets
if config.SCEP_SUBNETS:
from .scep import SCEPResource
app.add_route("/api/scep/", SCEPResource())
app.add_route("/api/scep/", SCEPResource(authority))
# Add sink for serving static files
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
if config.OCSP_SUBNETS:
from .ocsp import OCSPResource
app.add_sink(OCSPResource(), prefix="/api/ocsp")
app.add_sink(OCSPResource(authority), prefix="/api/ocsp")
# Set up log handlers
if config.LOGGING_BACKEND == "sql":
@ -273,7 +271,7 @@ def certidude_app(log_handlers=[]):
app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog":
from logging.handlers import SyslogHandler
log_handlers.append(SysLogHandler())
log_handlers.append(SyslogHandler())
# Browsing syslog via HTTP is obviously not possible out of the box
elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)

View File

@ -1,19 +1,18 @@
import click
import falcon
import logging
import re
from xattr import setxattr, listxattr, removexattr
from datetime import datetime
from certidude import config, authority, push
from certidude import 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
from certidude.auth import login_required, authorize_admin
from .utils.firewall import whitelist_subject
logger = logging.getLogger(__name__)
class AttributeResource(object):
def __init__(self, namespace):
def __init__(self, authority, namespace):
self.authority = authority
self.namespace = namespace
@serialize
@ -27,7 +26,7 @@ class AttributeResource(object):
Results made available only to lease IP address.
"""
try:
path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace)
path, buf, cert, attribs = self.authority.get_attributes(cn, namespace=self.namespace)
except IOError:
raise falcon.HTTPNotFound()
else:
@ -38,7 +37,7 @@ class AttributeResource(object):
def on_post(self, req, resp, cn):
namespace = ("user.%s." % self.namespace).encode("ascii")
try:
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
except IOError:
raise falcon.HTTPNotFound()
else:

View File

@ -1,14 +1,13 @@
import logging
from certidude.decorators import serialize
from certidude.config import cp
from certidude import authority, config, const
from certidude import config, const
from jinja2 import Template
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
class BootstrapResource(object):
class BootstrapResource(AuthorityHandler):
def on_get(self, req, resp):
resp.body = Template(open(config.BOOTSTRAP_TEMPLATE).read()).render(
authority = const.FQDN,
servers = authority.list_server_names())
servers = self.authority.list_server_names())

View File

@ -1,25 +1,24 @@
import click
import falcon
import logging
import os
import xattr
from datetime import datetime
from certidude import config, authority, push
from certidude import config, push
from certidude.auth import login_required, authorize_admin, authorize_server
from certidude.decorators import serialize
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
# TODO: lease namespacing (?)
class LeaseDetailResource(object):
class LeaseDetailResource(AuthorityHandler):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp, cn):
try:
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
return dict(
last_seen = xattr.getxattr(path, "user.lease.last_seen").decode("ascii"),
inner_address = xattr.getxattr(path, "user.lease.inner_address").decode("ascii"),
@ -29,7 +28,7 @@ class LeaseDetailResource(object):
raise falcon.HTTPNotFound()
class LeaseResource(object):
class LeaseResource(AuthorityHandler):
@authorize_server
def on_post(self, req, resp):
client_common_name = req.get_param("client", required=True)
@ -38,7 +37,7 @@ class LeaseResource(object):
if "," in client_common_name:
client_common_name, _ = client_common_name.split(",", 1)
path, buf, cert, signed, expires = authority.get_signed(client_common_name) # TODO: catch exceptions
path, buf, cert, signed, expires = self.authority.get_signed(client_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")
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

View File

@ -1,5 +1,4 @@
from certidude import config
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
from certidude.relational import RelationalMixin

View File

@ -1,18 +1,18 @@
import click
import falcon
import hashlib
import logging
import os
from asn1crypto.util import timezone
from asn1crypto import cms, algos, x509, ocsp
from base64 import b64decode, b64encode
from certbuilder import pem_armor_certificate
from certidude import authority, push, config
from certidude.firewall import whitelist_subnets
from datetime import datetime, timedelta
from oscrypto import keys, asymmetric, symmetric
from oscrypto.errors import SignatureError
from asn1crypto import ocsp
from base64 import b64decode
from certidude import config
from datetime import datetime
from oscrypto import asymmetric
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
class OCSPResource(object):
logger = logging.getLogger(__name__)
class OCSPResource(AuthorityHandler):
@whitelist_subnets(config.OCSP_SUBNETS)
def __call__(self, req, resp):
try:
@ -55,14 +55,14 @@ 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, signed, expires = authority.get_signed(link_target[3:-4])
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
if serial != cert.serial_number:
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, signed, expires, revoked = authority.get_revoked(serial)
path, buf, cert, signed, expires, revoked = self.authority.get_revoked(serial)
status = ocsp.CertStatus(
name='revoked',
value={
@ -102,7 +102,7 @@ class OCSPResource(object):
'certs': [server_certificate.asn1],
'signature_algorithm': {'algorithm': "sha1_rsa"},
'signature': asymmetric.rsa_pkcs1v15_sign(
authority.private_key,
self.authority.private_key,
response_data.dump(),
"sha1"
)

View File

@ -1,22 +1,21 @@
import click
import falcon
import logging
import ipaddress
import json
import os
import hashlib
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from base64 import b64decode
from certidude import config, authority, push, errors
from certidude import config, push, errors
from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import csrf_protection, MyEncoder, serialize
from certidude.firewall import whitelist_subnets, whitelist_content_types
from certidude.decorators import csrf_protection, MyEncoder
from datetime import datetime
from oscrypto import asymmetric
from oscrypto.errors import SignatureError
from xattr import getxattr
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets, whitelist_content_types
logger = logging.getLogger(__name__)
@ -27,7 +26,7 @@ curl -f -L -H "Content-type: application/pkcs10" --data-binary @test.csr \
http://ca.example.lan/api/request/?wait=yes
"""
class RequestListResource(object):
class RequestListResource(AuthorityHandler):
@login_optional
@whitelist_subnets(config.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10")
@ -61,7 +60,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)
cert, resp.body = self.authority._sign(csr, body, overwrite=True)
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
machine, req.context.get("remote_addr"))
return
@ -72,7 +71,7 @@ class RequestListResource(object):
Attempt to renew certificate using currently valid key pair
"""
try:
path, buf, cert, signed, expires = authority.get_signed(common_name)
path, buf, cert, signed, expires = self.authority.get_signed(common_name)
except EnvironmentError:
pass # No currently valid certificate for this common name
else:
@ -112,7 +111,7 @@ class RequestListResource(object):
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)
_, resp.body = self.authority._sign(csr, body, overwrite=True)
logger.info("Renewed certificate for %s", common_name)
return
@ -122,12 +121,12 @@ class RequestListResource(object):
autosigning was requested and certificate can be automatically signed
"""
if req.get_param_as_bool("autosign"):
if not authority.server_flags(common_name):
if not self.authority.server_flags(common_name):
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = authority._sign(csr, body)
_, resp.body = self.authority._sign(csr, body)
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return
except EnvironmentError:
@ -142,7 +141,7 @@ class RequestListResource(object):
# Attempt to save the request otherwise
try:
request_path, _, _ = authority.store_request(body,
request_path, _, _ = self.authority.store_request(body,
address=str(req.context.get("remote_addr")))
except errors.RequestExists:
reasons.append("Same request already uploaded exists")
@ -175,14 +174,14 @@ class RequestListResource(object):
cls=MyEncoder)
class RequestDetailResource(object):
class RequestDetailResource(AuthorityHandler):
def on_get(self, req, resp, cn):
"""
Fetch certificate signing request as PEM
"""
try:
path, buf, _, submitted = authority.get_request(cn)
path, buf, _, submitted = self.authority.get_request(cn)
except errors.RequestDoesNotExist:
logger.warning("Failed to serve non-existant request %s to %s",
cn, req.context.get("remote_addr"))
@ -206,7 +205,7 @@ class RequestDetailResource(object):
resp.body = json.dumps(dict(
submitted = submitted,
common_name = cn,
server = authority.server_flags(cn),
server = self.authority.server_flags(cn),
address = getxattr(path, "user.request.address").decode("ascii"), # TODO: move to authority.py
md5sum = hashlib.md5(buf).hexdigest(),
sha1sum = hashlib.sha1(buf).hexdigest(),
@ -225,7 +224,7 @@ class RequestDetailResource(object):
Sign a certificate signing request
"""
try:
cert, buf = authority.sign(cn,
cert, buf = self.authority.sign(cn,
profile=req.get_param("profile", default="default"),
overwrite=True,
signer=req.context.get("user").name)
@ -244,7 +243,7 @@ class RequestDetailResource(object):
@authorize_admin
def on_delete(self, req, resp, cn):
try:
authority.delete_request(cn)
self.authority.delete_request(cn)
# Logging implemented in the function above
except errors.RequestDoesNotExist as e:
resp.body = "No certificate signing request for %s found" % cn

View File

@ -1,15 +1,12 @@
import click
import falcon
import json
import logging
from certidude import const, config
from certidude.authority import export_crl, list_revoked
from certidude.firewall import whitelist_subnets
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
logger = logging.getLogger(__name__)
class RevocationListResource(object):
class RevocationListResource(AuthorityHandler):
@whitelist_subnets(config.CRL_SUBNETS)
def on_get(self, req, resp):
# Primarily offer DER encoded CRL as per RFC5280
@ -21,7 +18,7 @@ class RevocationListResource(object):
("attachment; filename=%s.crl" % const.HOSTNAME))
# Convert PEM to DER
logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr"))
resp.body = export_crl(pem=False)
resp.body = self.authority.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"
@ -35,7 +32,7 @@ class RevocationListResource(object):
"Content-Disposition",
("attachment; filename=%s-crl.pem" % const.HOSTNAME))
logger.debug("Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
resp.body = export_crl()
resp.body = self.authority.export_crl()
else:
logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))
raise falcon.HTTPUnsupportedMediaType(

View File

@ -1,14 +1,13 @@
import click
import hashlib
import os
from asn1crypto import cms, algos, x509
from asn1crypto.core import ObjectIdentifier, SetOf, PrintableString
from base64 import b64decode, b64encode
from certbuilder import pem_armor_certificate
from certidude import authority, push, config
from certidude.firewall import whitelist_subnets
from asn1crypto import cms, algos
from asn1crypto.core import SetOf, PrintableString
from base64 import b64decode
from certidude import config
from oscrypto import keys, asymmetric, symmetric
from oscrypto.errors import SignatureError
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
# Monkey patch asn1crypto
@ -30,18 +29,18 @@ cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
class SCEPError(Exception): code = 25 # system failure
class SCEPBadAlg(SCEPError): code = 0
class SCEPBadAlgo(SCEPError): code = 0
class SCEPBadMessageCheck(SCEPError): code = 1
class SCEPBadRequest(SCEPError): code = 2
class SCEPBadTime(SCEPError): code = 3
class SCEPBadCertId(SCEPError): code = 4
class SCEPResource(object):
class SCEPResource(AuthorityHandler):
@whitelist_subnets(config.SCEP_SUBNETS)
def on_get(self, req, resp):
operation = req.get_param("operation", required=True)
if operation.lower() == "getcacert":
resp.body = keys.parse_certificate(authority.certificate_buf).dump()
resp.body = keys.parse_certificate(self.authority.certificate_buf).dump()
resp.append_header("Content-Type", "application/x-x509-ca-cert")
return
@ -120,17 +119,17 @@ class SCEPResource(object):
encrypted_content = encrypted_content_info['encrypted_content'].native
recipient, = encrypted_envelope['recipient_infos']
if recipient.native["rid"]["serial_number"] != authority.certificate.serial_number:
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
raise SCEPBadCertId()
# Since CA private key is not directly readable here, we'll redirect it to signer socket
key = asymmetric.rsa_pkcs1v15_decrypt(
authority.private_key,
self.authority.private_key,
recipient.native["encrypted_key"])
if len(key) == 8: key = key * 3 # Convert DES to 3DES
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
_, _, common_name = authority.store_request(buf, overwrite=True)
cert, buf = authority.sign(common_name, overwrite=True)
_, _, common_name = self.authority.store_request(buf, overwrite=True)
cert, buf = self.authority.sign(common_name, overwrite=True)
signed_certificate = asymmetric.load_certificate(buf)
content = signed_certificate.asn1.dump()
@ -242,14 +241,14 @@ class SCEPResource(object):
'version': "v1",
'sid': cms.SignerIdentifier({
'issuer_and_serial_number': cms.IssuerAndSerialNumber({
'issuer': authority.certificate.issuer,
'serial_number': authority.certificate.serial_number,
'issuer': self.authority.certificate.issuer,
'serial_number': self.authority.certificate.serial_number,
}),
}),
'digest_algorithm': algos.DigestAlgorithm({'algorithm': "sha1"}),
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
'signature': asymmetric.rsa_pkcs1v15_sign(
authority.private_key,
self.authority.private_key,
b"\x31" + attrs.dump()[1:],
"sha1"
)
@ -260,7 +259,7 @@ class SCEPResource(object):
'content_type': "signed_data",
'content': cms.SignedData({
'version': "v1",
'certificates': [authority.certificate],
'certificates': [self.authority.certificate],
'digest_algorithms': [cms.DigestAlgorithm({
'algorithm': "sha1"
})],

View File

@ -1,18 +1,17 @@
import falcon
import logging
import os
from certidude import const, config, authority
from certidude.decorators import serialize
from certidude import const, config
from jinja2 import Environment, FileSystemLoader
from certidude.firewall import whitelist_subject
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subject
logger = logging.getLogger(__name__)
env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True)
class ScriptResource():
class ScriptResource(AuthorityHandler):
@whitelist_subject
def on_get(self, req, resp, cn):
path, buf, cert, attribs = authority.get_attributes(cn)
path, buf, cert, attribs = self.authority.get_attributes(cn)
# TODO: are keys unique?
named_tags = {}
other_tags = []

View File

@ -3,19 +3,19 @@ import falcon
import logging
import json
import hashlib
from certidude import authority
from certidude.auth import login_required, authorize_admin
from certidude.decorators import csrf_protection
from xattr import getxattr
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
class SignedCertificateDetailResource(object):
class SignedCertificateDetailResource(AuthorityHandler):
def on_get(self, req, resp, cn):
preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
try:
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
except EnvironmentError:
logger.warning("Failed to serve non-existant certificate %s to %s",
cn, req.context.get("remote_addr"))
@ -55,5 +55,5 @@ class SignedCertificateDetailResource(object):
def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr"))
authority.revoke(cn)
self.authority.revoke(cn)

View File

@ -1,18 +1,18 @@
import falcon
import logging
from xattr import getxattr, removexattr, setxattr
from certidude import authority, push
from certidude import push
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize, csrf_protection
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
class TagResource(object):
class TagResource(AuthorityHandler):
@serialize
@login_required
@authorize_admin
def on_get(self, req, resp, cn):
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
tags = []
try:
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
@ -30,7 +30,7 @@ class TagResource(object):
@login_required
@authorize_admin
def on_post(self, req, resp, cn):
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.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(","))
@ -46,11 +46,14 @@ class TagResource(object):
class TagDetailResource(object):
def __init__(self, authority):
self.authority = authority
@csrf_protection
@login_required
@authorize_admin
def on_put(self, req, resp, cn, tag):
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
value = req.get_param("value", required=True)
try:
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
@ -72,7 +75,7 @@ class TagDetailResource(object):
@login_required
@authorize_admin
def on_delete(self, req, resp, cn, tag):
path, buf, cert, signed, expires = authority.get_signed(cn)
path, buf, cert, signed, expires = self.authority.get_signed(cn)
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
tags.remove(tag)
if not tags:

View File

@ -1,9 +1,6 @@
import click
import falcon
import logging
import hashlib
import random
import string
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from datetime import datetime
@ -11,12 +8,13 @@ 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 import config
from certidude.auth import login_required, authorize_admin
from .utils import AuthorityHandler
logger = logging.getLogger(__name__)
class TokenResource(object):
class TokenResource(AuthorityHandler):
def on_put(self, req, resp):
# Consume token
now = time()
@ -43,7 +41,7 @@ class TokenResource(object):
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.body = self.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:

View File

@ -0,0 +1,3 @@
class AuthorityHandler:
def __init__(self, authority):
self.authority = authority

View File

@ -1,7 +1,6 @@
import falcon
import logging
import click
from asn1crypto import pem, x509
logger = logging.getLogger("api")
@ -10,8 +9,6 @@ def whitelist_subnets(subnets):
"""
Validate source IP address of API call against subnet list
"""
import falcon
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
# Check for administration subnet whitelist
@ -30,8 +27,6 @@ def whitelist_subnets(subnets):
return wrapper
def whitelist_content_types(*content_types):
import falcon
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
for content_type in content_types:
@ -58,7 +53,7 @@ def whitelist_subject(func):
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
origin_cert = x509.Certificate.load(der_bytes)
if origin_cert.native == cert.native:
click.echo("Subject authenticated using certificates")
logger.debug("Subject authenticated using certificates")
return func(self, req, resp, cn, *args, **kwargs)
# For backwards compatibility check source IP address
@ -73,4 +68,3 @@ def whitelist_subject(func):
else:
return func(self, req, resp, cn, *args, **kwargs)
return wrapped

View File

@ -9,7 +9,6 @@ import re
import socket
from base64 import b64decode
from certidude.user import User
from certidude.firewall import whitelist_subnets
from certidude import config, const
logger = logging.getLogger("api")