1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-23 00:25:18 +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 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 mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04
- sudo easy_install3 pip - sudo easy_install3 pip
- sudo pip3 install -r requirements.txt - sudo -H pip3 install -r requirements.txt
- sudo pip3 install codecov pytest-cov requests-kerberos - sudo -H pip3 install codecov pytest-cov requests-kerberos
- sudo pip3 install -e . - sudo -H pip3 install -e .
- echo ca | sudo tee /etc/hostname - echo ca | sudo tee /etc/hostname
- echo 127.0.0.1 localhost | sudo tee /etc/hosts - echo 127.0.0.1 localhost | sudo tee /etc/hosts
- echo 127.0.1.1 ca.example.lan ca | sudo tee -a /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 combine
- sudo coverage report - sudo coverage report
- sudo coverage xml -i - sudo coverage xml -i
cache: cache: pip
directories:
- $HOME/.cache/pip

View File

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

View File

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

View File

@ -1,14 +1,13 @@
import logging import logging
from certidude.decorators import serialize from certidude import config, const
from certidude.config import cp
from certidude import authority, config, const
from jinja2 import Template from jinja2 import Template
from .utils import AuthorityHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BootstrapResource(object): class BootstrapResource(AuthorityHandler):
def on_get(self, req, resp): def on_get(self, req, resp):
resp.body = Template(open(config.BOOTSTRAP_TEMPLATE).read()).render( resp.body = Template(open(config.BOOTSTRAP_TEMPLATE).read()).render(
authority = const.FQDN, 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 falcon
import logging import logging
import os import os
import xattr import xattr
from datetime import datetime 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.auth import login_required, authorize_admin, authorize_server
from certidude.decorators import serialize from certidude.decorators import serialize
from .utils import AuthorityHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: lease namespacing (?) # TODO: lease namespacing (?)
class LeaseDetailResource(object): class LeaseDetailResource(AuthorityHandler):
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
try: try:
path, buf, cert, signed, expires = authority.get_signed(cn) path, buf, cert, signed, expires = self.authority.get_signed(cn)
return dict( return dict(
last_seen = xattr.getxattr(path, "user.lease.last_seen").decode("ascii"), last_seen = xattr.getxattr(path, "user.lease.last_seen").decode("ascii"),
inner_address = xattr.getxattr(path, "user.lease.inner_address").decode("ascii"), inner_address = xattr.getxattr(path, "user.lease.inner_address").decode("ascii"),
@ -29,7 +28,7 @@ class LeaseDetailResource(object):
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
class LeaseResource(object): class LeaseResource(AuthorityHandler):
@authorize_server @authorize_server
def on_post(self, req, resp): def on_post(self, req, resp):
client_common_name = req.get_param("client", required=True) client_common_name = req.get_param("client", required=True)
@ -38,7 +37,7 @@ class LeaseResource(object):
if "," in client_common_name: if "," in client_common_name:
client_common_name, _ = client_common_name.split(",", 1) 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 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") raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied")
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 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.auth import login_required, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.relational import RelationalMixin from certidude.relational import RelationalMixin

View File

@ -1,18 +1,18 @@
import click
import falcon import falcon
import hashlib import logging
import os import os
from asn1crypto.util import timezone from asn1crypto.util import timezone
from asn1crypto import cms, algos, x509, ocsp from asn1crypto import ocsp
from base64 import b64decode, b64encode from base64 import b64decode
from certbuilder import pem_armor_certificate from certidude import config
from certidude import authority, push, config from datetime import datetime
from certidude.firewall import whitelist_subnets from oscrypto import asymmetric
from datetime import datetime, timedelta from .utils import AuthorityHandler
from oscrypto import keys, asymmetric, symmetric from .utils.firewall import whitelist_subnets
from oscrypto.errors import SignatureError
class OCSPResource(object): logger = logging.getLogger(__name__)
class OCSPResource(AuthorityHandler):
@whitelist_subnets(config.OCSP_SUBNETS) @whitelist_subnets(config.OCSP_SUBNETS)
def __call__(self, req, resp): def __call__(self, req, resp):
try: try:
@ -55,14 +55,14 @@ class OCSPResource(object):
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial)) link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial))
assert link_target.startswith("../") assert link_target.startswith("../")
assert link_target.endswith(".pem") 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: if serial != cert.serial_number:
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %x" % (link_target, 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") raise EnvironmentError("Integrity check failed")
status = ocsp.CertStatus(name='good', value=None) status = ocsp.CertStatus(name='good', value=None)
except EnvironmentError: except EnvironmentError:
try: 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( status = ocsp.CertStatus(
name='revoked', name='revoked',
value={ value={
@ -102,7 +102,7 @@ class OCSPResource(object):
'certs': [server_certificate.asn1], 'certs': [server_certificate.asn1],
'signature_algorithm': {'algorithm': "sha1_rsa"}, 'signature_algorithm': {'algorithm': "sha1_rsa"},
'signature': asymmetric.rsa_pkcs1v15_sign( 'signature': asymmetric.rsa_pkcs1v15_sign(
authority.private_key, self.authority.private_key,
response_data.dump(), response_data.dump(),
"sha1" "sha1"
) )

View File

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

View File

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

View File

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

View File

@ -1,18 +1,17 @@
import falcon
import logging import logging
import os import os
from certidude import const, config, authority from certidude import const, config
from certidude.decorators import serialize
from jinja2 import Environment, FileSystemLoader 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__) logger = logging.getLogger(__name__)
env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True)
class ScriptResource(): class ScriptResource(AuthorityHandler):
@whitelist_subject @whitelist_subject
def on_get(self, req, resp, cn): 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? # TODO: are keys unique?
named_tags = {} named_tags = {}
other_tags = [] other_tags = []

View File

@ -3,19 +3,19 @@ import falcon
import logging import logging
import json import json
import hashlib import hashlib
from certidude import authority
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.decorators import csrf_protection from certidude.decorators import csrf_protection
from xattr import getxattr from xattr import getxattr
from .utils import AuthorityHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SignedCertificateDetailResource(object): class SignedCertificateDetailResource(AuthorityHandler):
def on_get(self, req, resp, cn): def on_get(self, req, resp, cn):
preferred_type = req.client_prefers(("application/json", "application/x-pem-file")) preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
try: try:
path, buf, cert, signed, expires = authority.get_signed(cn) path, buf, cert, signed, expires = self.authority.get_signed(cn)
except EnvironmentError: except EnvironmentError:
logger.warning("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")) cn, req.context.get("remote_addr"))
@ -55,5 +55,5 @@ class SignedCertificateDetailResource(object):
def on_delete(self, req, resp, cn): def on_delete(self, req, resp, cn):
logger.info("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")) 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 import logging
from xattr import getxattr, removexattr, setxattr 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.auth import login_required, authorize_admin
from certidude.decorators import serialize, csrf_protection from certidude.decorators import serialize, csrf_protection
from .utils import AuthorityHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TagResource(object): class TagResource(AuthorityHandler):
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp, cn): 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 = [] tags = []
try: try:
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","): for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
@ -30,7 +30,7 @@ class TagResource(object):
@login_required @login_required
@authorize_admin @authorize_admin
def on_post(self, req, resp, cn): 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) key, value = req.get_param("key", required=True), req.get_param("value", required=True)
try: try:
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
@ -46,11 +46,14 @@ class TagResource(object):
class TagDetailResource(object): class TagDetailResource(object):
def __init__(self, authority):
self.authority = authority
@csrf_protection @csrf_protection
@login_required @login_required
@authorize_admin @authorize_admin
def on_put(self, req, resp, cn, tag): 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) value = req.get_param("value", required=True)
try: try:
tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
@ -72,7 +75,7 @@ class TagDetailResource(object):
@login_required @login_required
@authorize_admin @authorize_admin
def on_delete(self, req, resp, cn, tag): 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 = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(","))
tags.remove(tag) tags.remove(tag)
if not tags: if not tags:

View File

@ -1,9 +1,6 @@
import click
import falcon import falcon
import logging import logging
import hashlib import hashlib
import random
import string
from asn1crypto import pem from asn1crypto import pem
from asn1crypto.csr import CertificationRequest from asn1crypto.csr import CertificationRequest
from datetime import datetime from datetime import datetime
@ -11,12 +8,13 @@ from time import time
from certidude import mailer from certidude import mailer
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.user import User from certidude.user import User
from certidude import config, authority from certidude import config
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from .utils import AuthorityHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TokenResource(object): class TokenResource(AuthorityHandler):
def on_put(self, req, resp): def on_put(self, req, resp):
# Consume token # Consume token
now = time() now = time()
@ -43,7 +41,7 @@ class TokenResource(object):
common_name = csr["certification_request_info"]["subject"].native["common_name"] 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 assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
try: try:
_, resp.body = authority._sign(csr, body) _, resp.body = self.authority._sign(csr, body)
resp.set_header("Content-Type", "application/x-pem-file") resp.set_header("Content-Type", "application/x-pem-file")
logger.info("Autosigned %s as proven by token ownership", common_name) logger.info("Autosigned %s as proven by token ownership", common_name)
except FileExistsError: 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 falcon
import logging import logging
import click
from asn1crypto import pem, x509 from asn1crypto import pem, x509
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -10,8 +9,6 @@ def whitelist_subnets(subnets):
""" """
Validate source IP address of API call against subnet list Validate source IP address of API call against subnet list
""" """
import falcon
def wrapper(func): def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs): def wrapped(self, req, resp, *args, **kwargs):
# Check for administration subnet whitelist # Check for administration subnet whitelist
@ -30,8 +27,6 @@ def whitelist_subnets(subnets):
return wrapper return wrapper
def whitelist_content_types(*content_types): def whitelist_content_types(*content_types):
import falcon
def wrapper(func): def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs): def wrapped(self, req, resp, *args, **kwargs):
for content_type in content_types: for content_type in content_types:
@ -58,7 +53,7 @@ def whitelist_subject(func):
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii")) header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
origin_cert = x509.Certificate.load(der_bytes) origin_cert = x509.Certificate.load(der_bytes)
if origin_cert.native == cert.native: 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) return func(self, req, resp, cn, *args, **kwargs)
# For backwards compatibility check source IP address # For backwards compatibility check source IP address
@ -73,4 +68,3 @@ def whitelist_subject(func):
else: else:
return func(self, req, resp, cn, *args, **kwargs) return func(self, req, resp, cn, *args, **kwargs)
return wrapped return wrapped

View File

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