mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
commit
a1f7b5fca5
10
.travis.yml
10
.travis.yml
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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"
|
||||||
})],
|
})],
|
||||||
|
@ -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 = []
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
3
certidude/api/utils/__init__.py
Normal file
3
certidude/api/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
class AuthorityHandler:
|
||||||
|
def __init__(self, authority):
|
||||||
|
self.authority = authority
|
@ -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
|
||||||
|
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user