diff --git a/.travis.yml b/.travis.yml index 2fe05f9..cd1593c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 81d04ed..f1de85a 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -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) diff --git a/certidude/api/attrib.py b/certidude/api/attrib.py index 28c0b50..fde9931 100644 --- a/certidude/api/attrib.py +++ b/certidude/api/attrib.py @@ -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: diff --git a/certidude/api/bootstrap.py b/certidude/api/bootstrap.py index d1ba52d..c69ebed 100644 --- a/certidude/api/bootstrap.py +++ b/certidude/api/bootstrap.py @@ -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()) diff --git a/certidude/api/lease.py b/certidude/api/lease.py index 7ba1bf9..89336a8 100644 --- a/certidude/api/lease.py +++ b/certidude/api/lease.py @@ -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" diff --git a/certidude/api/log.py b/certidude/api/log.py index 7faaec3..1d925a3 100644 --- a/certidude/api/log.py +++ b/certidude/api/log.py @@ -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 diff --git a/certidude/api/ocsp.py b/certidude/api/ocsp.py index 26c2f4a..508fb20 100644 --- a/certidude/api/ocsp.py +++ b/certidude/api/ocsp.py @@ -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" ) diff --git a/certidude/api/request.py b/certidude/api/request.py index 7424eb9..e2d0640 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -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 diff --git a/certidude/api/revoked.py b/certidude/api/revoked.py index 5851848..44cb721 100644 --- a/certidude/api/revoked.py +++ b/certidude/api/revoked.py @@ -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( diff --git a/certidude/api/scep.py b/certidude/api/scep.py index 7c1aa95..dee63ba 100644 --- a/certidude/api/scep.py +++ b/certidude/api/scep.py @@ -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" })], diff --git a/certidude/api/script.py b/certidude/api/script.py index 494e528..db3f4bf 100644 --- a/certidude/api/script.py +++ b/certidude/api/script.py @@ -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 = [] diff --git a/certidude/api/signed.py b/certidude/api/signed.py index 07ea373..2b42cc6 100644 --- a/certidude/api/signed.py +++ b/certidude/api/signed.py @@ -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) diff --git a/certidude/api/tag.py b/certidude/api/tag.py index a670588..8ea7454 100644 --- a/certidude/api/tag.py +++ b/certidude/api/tag.py @@ -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: diff --git a/certidude/api/token.py b/certidude/api/token.py index 27e2d33..13ce10a 100644 --- a/certidude/api/token.py +++ b/certidude/api/token.py @@ -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: diff --git a/certidude/api/utils/__init__.py b/certidude/api/utils/__init__.py new file mode 100644 index 0000000..781c48d --- /dev/null +++ b/certidude/api/utils/__init__.py @@ -0,0 +1,3 @@ +class AuthorityHandler: + def __init__(self, authority): + self.authority = authority diff --git a/certidude/firewall.py b/certidude/api/utils/firewall.py similarity index 95% rename from certidude/firewall.py rename to certidude/api/utils/firewall.py index 6a16d49..d714b68 100644 --- a/certidude/firewall.py +++ b/certidude/api/utils/firewall.py @@ -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 - diff --git a/certidude/auth.py b/certidude/auth.py index e57208d..e0abc4e 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -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")