Refactor codebase

* Replace PyOpenSSL with cryptography.io
* Rename constants to const
* Drop support for uwsgi
* Use systemd to launch certidude server
* Signer automatically spawned as part of server
* Update requirements.txt
* Clean up certidude client configuration handling
* Add automatic enroll with Kerberos machine cerdentials
This commit is contained in:
Lauri Võsandi 2016-09-18 00:00:14 +03:00
parent 15858083b3
commit b4d006227a
35 changed files with 1181 additions and 1057 deletions

View File

@ -1,4 +1,6 @@
include README.rst
include certidude/templates/*.sh
include certidude/templates/*.service
include certidude/templates/*.ovpn
include certidude/templates/*.conf
include certidude/templates/*.ini

View File

@ -79,7 +79,8 @@ To install Certidude:
python-pysqlite2 python-mysql.connector python-ldap \
build-essential libffi-dev libssl-dev libkrb5-dev \
ldap-utils krb5-user \
libsasl2-modules-gssapi-mit
libsasl2-modules-gssapi-mit \
libsasl2-dev libldap2-dev
pip install certidude
@ -103,17 +104,18 @@ If necessary tweak machine's fully qualified hostname in ``/etc/hosts``:
127.0.0.1 localhost
127.0.1.1 ca.example.com ca
Then proceed to install `nchan <https://nchan.slact.net/>`_ and ``uwsgi``:
Then proceed to install `nchan <https://nchan.slact.net/>`_:
.. code:: bash
wget https://nchan.slact.net/download/nginx-common.deb https://nchan.slact.net/download/nginx-extras.deb
wget https://nchan.slact.net/download/nginx-common.deb \
https://nchan.slact.net/download/nginx-extras.deb
dpkg -i nginx-common.deb nginx-extras.deb
apt-get install nginx uwsgi uwsgi-plugin-python
apt-get -f install
Certidude can set up certificate authority relatively easily.
Following will set up certificate authority in ``/var/lib/certidude/hostname.domain.tld``,
configure uWSGI in ``/etc/uwsgi/apps-available/certidude.ini``,
configure gunicorn service for your platform,
nginx in ``/etc/nginx/sites-available/certidude.conf``,
cronjobs in ``/etc/cron.hourly/certidude`` and much more:
@ -170,7 +172,8 @@ Install dependencies:
apt-get install samba-common-bin krb5-user ldap-utils
Reset Samba client configuration in ``/etc/samba/smb.conf``:
Reset Samba client configuration in ``/etc/samba/smb.conf``, adjust
workgroup and realm accordingly:
.. code:: ini
@ -190,6 +193,13 @@ Reset Kerberos configuration in ``/etc/krb5.conf``:
dns_lookup_realm = true
dns_lookup_kdc = true
Reset LDAP configuration in /etc/ldap/ldap.conf:
.. code:: bash
BASE dc=example,dc=com
URI ldap://dc1.example.com
Initialize Kerberos credentials:
.. code:: bash
@ -230,6 +240,11 @@ Adjust admin filter according to your setup.
Also make sure there is cron.hourly job for creating GSSAPI credential cache -
that's necessary for querying LDAP using Certidude machine's credentials.
Common pitfalls:
* Following error message may mean that the IP address of the web server does not match the IP address used to join
the CA machine to domain, eg when you're running CA behind SSL terminating web server:
Bad credentials: Unspecified GSS failure. Minor code may provide more information (851968)
Automating certificate setup
----------------------------

View File

@ -12,7 +12,7 @@ from certidude.auth import login_required, authorize_admin
from certidude.user import User
from certidude.decorators import serialize, event_source, csrf_protection
from certidude.wrappers import Request, Certificate
from certidude import constants, config
from certidude import const, config
logger = logging.getLogger("api")
@ -35,7 +35,7 @@ class CertificateAuthorityResource(object):
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
resp.append_header("Content-Type", "application/x-x509-ca-cert")
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
constants.HOSTNAME.encode("ascii"))
const.HOSTNAME.encode("ascii"))
class SessionResource(object):
@ -112,7 +112,7 @@ class NormalizeMiddleware(object):
assert not req.get_param("unicode") or req.get_param("unicode") == u"", "Unicode sanity check failed"
req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8"))
def process_response(self, req, resp, resource):
def process_response(self, req, resp, resource=None):
# wtf falcon?!
if isinstance(resp.location, unicode):
resp.location = resp.location.encode("ascii")
@ -125,7 +125,6 @@ def certidude_app():
from .request import RequestListResource, RequestDetailResource
from .lease import LeaseResource
from .whois import WhoisResource
from .log import LogResource
from .tag import TagResource, TagDetailResource
from .cfg import ConfigResource, ScriptResource
@ -149,19 +148,6 @@ def certidude_app():
if config.USER_CERTIFICATE_ENROLLMENT:
app.add_route("/api/bundle/", BundleResource())
log_handlers = []
if config.LOGGING_BACKEND == "sql":
from certidude.mysqllog import LogHandler
uri = config.cp.get("logging", "database")
log_handlers.append(LogHandler(uri))
app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog":
from logging.handlers import SyslogHandler
log_handlers.append(SysLogHandler())
# Browsing syslog via HTTP is obviously not possible out of the box
elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
if config.TAGGING_BACKEND == "sql":
uri = config.cp.get("tagging", "database")
app.add_route("/api/tag/", TagResource(uri))
@ -171,23 +157,5 @@ def certidude_app():
elif config.TAGGING_BACKEND:
raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND)
if config.PUSH_PUBLISH:
from certidude.push import PushLogHandler
log_handlers.append(PushLogHandler())
for facility in "api", "cli":
logger = logging.getLogger(facility)
logger.setLevel(logging.DEBUG)
for handler in log_handlers:
logger.addHandler(handler)
logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN)
import atexit
def exit_handler():
logging.getLogger("cli").debug("Shutting down Certidude")
atexit.register(exit_handler)
return app

View File

@ -1,4 +1,5 @@
import logging
import hashlib
from certidude import config, authority

View File

@ -36,8 +36,6 @@ join
tag on device_tag.tag_id = tag.id
join
device on device_tag.device_id = device.id
where
device.cn = %s
"""
@ -63,7 +61,7 @@ class ConfigResource(RelationalMixin):
@login_required
@authorize_admin
def on_get(self, req, resp):
return self.iterfetch(SQL_SELECT_RULES)
return self.iterfetch(SQL_SELECT_TAGS)
class ScriptResource(RelationalMixin):

View File

@ -10,6 +10,9 @@ from certidude.decorators import serialize, csrf_protection
from certidude.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types
from cryptography import x509
from cryptography.hazmat.backends import default_backend
logger = logging.getLogger("api")
class RequestListResource(object):
@ -29,6 +32,7 @@ class RequestListResource(object):
"""
body = req.stream.read(req.content_length)
csr = Request(body)
if not csr.common_name:
@ -38,6 +42,19 @@ class RequestListResource(object):
"Bad request",
"No common name specified!")
machine = req.context.get("machine")
if machine:
if csr.common_name != machine:
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine))
if csr.signable:
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr, overwrite=True).dump()
return
# Check if this request has been already signed and return corresponding certificte if it has been signed
try:
cert = authority.get_signed(csr.common_name)
@ -51,8 +68,8 @@ class RequestListResource(object):
# TODO: check for revoked certificates and return HTTP 410 Gone
# Process automatic signing if the IP address is whitelisted and autosigning was requested
if req.get_param_as_bool("autosign"):
# Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed
if req.get_param_as_bool("autosign") and csr.signable:
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:

View File

@ -2,7 +2,7 @@
import falcon
import json
import logging
from certidude import constants
from certidude import const
from certidude.authority import export_crl, list_revoked
from certidude.decorators import MyEncoder
from cryptography import x509
@ -21,7 +21,7 @@ class RevocationListResource(object):
resp.set_header("Content-Type", "application/x-pkcs7-crl")
resp.append_header(
"Content-Disposition",
("attachment; filename=%s.crl" % constants.HOSTNAME).encode("ascii"))
("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii"))
# Convert PEM to DER
resp.body = x509.load_pem_x509_crl(export_crl(),
default_backend()).public_bytes(Encoding.DER)
@ -29,7 +29,7 @@ class RevocationListResource(object):
resp.set_header("Content-Type", "application/x-pem-file")
resp.append_header(
"Content-Disposition",
("attachment; filename=%s-crl.pem" % constants.HOSTNAME).encode("ascii"))
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
resp.body = export_crl()
elif req.accept.startswith("application/json"):
resp.set_header("Content-Type", "application/json")

View File

@ -1,14 +1,14 @@
import click
import falcon
import kerberos
import kerberos # If this fails pip install kerberos
import logging
import os
import re
import socket
from certidude.user import User
from certidude.firewall import whitelist_subnets
from certidude import config, constants
from certidude import config, const
logger = logging.getLogger("api")
@ -23,32 +23,34 @@ if "kerberos" in config.AUTHENTICATION_BACKENDS:
exit(248)
try:
principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN)
principal = kerberos.getServerPrincipalDetails("HTTP", const.FQDN)
except kerberos.KrbError as exc:
click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (
constants.FQDN, exc), err=True)
const.FQDN, exc), err=True)
exit(249)
else:
click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.FQDN)
click.echo("Kerberos enabled, service principal is HTTP/%s" % const.FQDN)
def authenticate(optional=False):
def wrapper(func):
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
# Try pre-emptive authentication
if not req.auth:
resp.append_header("WWW-Authenticate", "Negotiate")
if optional:
req.context["user"] = None
return func(resource, req, resp, *args, **kwargs)
logger.debug(u"No Kerberos ticket offered while attempting to access %s from %s",
req.env["PATH_INFO"], req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized",
"No Kerberos ticket offered, are you sure you've logged in with domain user account?")
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
["Negotiate"])
token = ''.join(req.auth.split()[1:])
try:
result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN)
result, context = kerberos.authGSSServerInit("HTTP@" + const.FQDN)
except kerberos.GSSError as ex:
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
@ -59,22 +61,30 @@ def authenticate(optional=False):
except kerberos.GSSError as ex:
kerberos.authGSSServerClean(context)
logger.error(u"Kerberos authentication failed from %s. "
"Bad credentials: %s (%d)",
"GSSAPI error: %s (%d), perhaps the clock skew it too large?",
req.context.get("remote_addr"),
ex.args[0][0], ex.args[0][1])
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1]))
"GSSAPI error: %s (%d), perhaps the clock skew it too large?" % (ex.args[0][0], ex.args[0][1]))
except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context)
logger.error(u"Kerberos authentication failed from %s. "
"Bad credentials: %s (%d)",
"Kerberos error: %s (%d)",
req.context.get("remote_addr"),
ex.args[0][0], ex.args[0][1])
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s" % (ex.args[0],))
"Kerberos error: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context)
req.context["user"] = User.objects.get(user)
if "$@" in user and optional:
# Extract machine hostname
# TODO: Assert LDAP group membership
req.context["machine"], _ = user.lower().split("$@", 1)
req.context["user"] = None
else:
# Attempt to look up real user
req.context["user"] = User.objects.get(user)
try:
kerberos.authGSSServerClean(context)
@ -114,7 +124,7 @@ def authenticate(optional=False):
if not req.auth:
resp.append_header("WWW-Authenticate", "Basic")
raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN)
if not req.auth.startswith("Basic "):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
@ -128,13 +138,13 @@ def authenticate(optional=False):
conn = ldap.initialize(server)
conn.set_option(ldap.OPT_REFERRALS, 0)
try:
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, const.DOMAIN), passwd)
except ldap.LDAPError, e:
resp.append_header("WWW-Authenticate", "Basic")
logger.critical(u"LDAP bind authentication failed for user %s from %s",
repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN)
req.context["ldap_conn"] = conn
break
@ -154,8 +164,7 @@ def authenticate(optional=False):
return func(resource, req, resp, *args, **kwargs)
if not req.auth:
resp.append_header("WWW-Authenticate", "Basic")
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate")
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",))
if not req.auth.startswith("Basic "):
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
@ -168,7 +177,7 @@ def authenticate(optional=False):
if not simplepam.authenticate(user, passwd, "sshd"):
logger.critical(u"Basic authentication failed for user %s from %s",
repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
raise falcon.HTTPForbidden("Forbidden", "Invalid password")
req.context["user"] = User.objects.get(user)
return func(resource, req, resp, *args, **kwargs)

View File

@ -1,13 +1,18 @@
import click
import os
import random
import re
import socket
import requests
from OpenSSL import crypto
from certidude import config, push, mailer
import socket
from datetime import datetime, timedelta
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID, AuthorityInformationAccessOID
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes, serialization
from certidude import config, push, mailer, const
from certidude.wrappers import Certificate, Request
from certidude.signer import raw_sign
from certidude import errors
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
@ -46,7 +51,7 @@ def publish_certificate(func):
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
# For deleting request in the web view, use pubkey modulo
push.publish("request-signed", csr.common_name)
push.publish("request-signed", cert.common_name)
return cert
return wrapped
@ -73,8 +78,16 @@ def store_request(buf, overwrite=False):
"""
Store CSR for later processing
"""
request = crypto.load_certificate_request(crypto.FILETYPE_PEM, buf)
common_name = request.get_subject().CN
if not buf: return # No certificate supplied
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
for name in csr.subject:
if name.oid == NameOID.COMMON_NAME:
common_name = name.value
break
else:
raise ValueError("No common name in %s" % csr.subject)
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
if not re.match(RE_HOSTNAME, common_name):
@ -98,7 +111,7 @@ def store_request(buf, overwrite=False):
def signer_exec(cmd, *bits):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(config.SIGNER_SOCKET_PATH)
sock.connect(const.SIGNER_SOCKET_PATH)
sock.send(cmd.encode("ascii"))
sock.send(b"\n")
for bit in bits:
@ -141,7 +154,7 @@ def list_revoked(directory=config.REVOKED_DIR):
def export_crl():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(config.SIGNER_SOCKET_PATH)
sock.connect(const.SIGNER_SOCKET_PATH)
sock.send(b"export-crl\n")
for filename in os.listdir(config.REVOKED_DIR):
if not filename.endswith(".pem"):
@ -177,32 +190,49 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
"""
Generate private key, sign certificate and return PKCS#12 bundle
"""
# Construct private key
click.echo("Generating %d-bit RSA key..." % key_size)
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, key_size)
# Construct CSR
csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key)
csr.get_subject().CN = common_name
key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(k, v) for k, v in (
(NameOID.COMMON_NAME, common_name),
(NameOID.GIVEN_NAME, owner and owner.given_name),
(NameOID.SURNAME, owner and owner.surname),
) if v
]))
if owner:
if owner.given_name:
csr.get_subject().GN = owner.given_name
if owner.surname:
csr.get_subject().SN = owner.surname
csr.add_extensions([
crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail.encode("ascii"))])
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
click.echo("Setting e-mail to: %s" % owner.mail)
csr = csr.add_extension(
x509.SubjectAlternativeName([
x509.RFC822Name(owner.mail)
]),
critical=False)
# Sign CSR
cert = sign(Request(buf), overwrite=True)
cert = sign(Request(
csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True)
# Generate P12
# Generate P12, currently supported only by PyOpenSSL
from OpenSSL import crypto
p12 = crypto.PKCS12()
p12.set_privatekey( key )
p12.set_privatekey(
crypto.load_privatekey(
crypto.FILETYPE_PEM,
key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
)
)
p12.set_certificate( cert._obj )
p12.set_ca_certificates([certificate._obj])
return p12.export(), cert
@ -213,7 +243,6 @@ def sign(req, overwrite=False, delete=True):
"""
Sign certificate signing request via signer process
"""
cert_path = os.path.join(config.SIGNED_DIR, req.common_name + ".pem")
# Move existing certificate if necessary
@ -236,35 +265,95 @@ def sign(req, overwrite=False, delete=True):
@publish_certificate
def sign2(request, overwrite=False, delete=True, lifetime=None):
def sign2(request, private_key, authority_certificate, overwrite=False, delete=True, lifetime=None):
"""
Sign directly using private key, this is usually done by root.
Basic constraints and certificate lifetime are copied from config,
lifetime may be overridden on the command line,
other extensions are copied as is.
"""
cert = raw_sign(
crypto.load_privatekey(crypto.FILETYPE_PEM, open(config.AUTHORITY_PRIVATE_KEY_PATH).read()),
crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()),
request._obj,
config.CERTIFICATE_BASIC_CONSTRAINTS,
lifetime=lifetime or config.CERTIFICATE_LIFETIME)
path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem")
if os.path.exists(path):
certificate_path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem")
if os.path.exists(certificate_path):
if overwrite:
revoke_certificate(request.common_name)
else:
raise EnvironmentError("File %s already exists!" % path)
raise errors.DuplicateCommonNameError("Valid certificate with common name %s already exists" % request.common_name)
buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
with open(path + ".part", "wb") as fh:
now = datetime.utcnow()
request_path = os.path.join(config.REQUESTS_DIR, request.common_name + ".pem")
request = x509.load_pem_x509_csr(open(request_path).read(), default_backend())
cert = x509.CertificateBuilder(
).subject_name(x509.Name([n for n in request.subject])
).serial_number(random.randint(
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff)
).issuer_name(authority_certificate.issuer
).public_key(request.public_key()
).not_valid_before(now - timedelta(hours=1)
).not_valid_after(now + timedelta(days=config.CERTIFICATE_LIFETIME)
).add_extension(x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False), critical=True
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(request.public_key()),
critical=False
).add_extension(
x509.AuthorityInformationAccess([
x509.AccessDescription(
AuthorityInformationAccessOID.CA_ISSUERS,
x509.UniformResourceIdentifier(
config.CERTIFICATE_AUTHORITY_URL)
)
]),
critical=False
).add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[
x509.UniformResourceIdentifier(
config.CERTIFICATE_CRL_URL)],
relative_name=None,
crl_issuer=None,
reasons=None)
]),
critical=False
).add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(
authority_certificate.public_key()),
critical=False
)
# Append subject alternative name, extended key usage flags etc
for extension in request.extensions:
if extension.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
click.echo("Appending subject alt name extension: %s" % extension)
cert = cert.add_extension(x509.SubjectAlternativeName(extension.value),
critical=extension.critical)
if extension.oid == ExtensionOID.EXTENDED_KEY_USAGE:
click.echo("Appending extended key usage flags extension: %s" % extension)
cert = cert.add_extension(x509.ExtendedKeyUsage(extension.value),
critical=extension.critical)
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
buf = cert.public_bytes(serialization.Encoding.PEM)
with open(certificate_path + ".part", "wb") as fh:
fh.write(buf)
os.rename(path + ".part", path)
click.echo("Wrote certificate to: %s" % path)
os.rename(certificate_path + ".part", certificate_path)
click.echo("Wrote certificate to: %s" % certificate_path)
if delete:
os.unlink(request.path)
click.echo("Deleted request: %s" % request.path)
os.unlink(request_path)
click.echo("Deleted request: %s" % request_path)
return Certificate(open(path))
return Certificate(open(certificate_path))

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,13 @@ import configparser
import ipaddress
import os
import string
import const
from random import choice
from urllib.parse import urlparse
cp = configparser.ConfigParser()
cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8"))
# Options that are parsed from config file are fetched here
cp = configparser.RawConfigParser()
cp.readfp(codecs.open(const.CONFIG_PATH, "r", "utf8"))
AUTHENTICATION_BACKENDS = set([j for j in
cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap
@ -28,9 +30,6 @@ AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
SIGNER_PID_PATH = "/run/certidude/signer.pid"
AUTHORITY_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
@ -49,16 +48,13 @@ USER_MULTIPLE_CERTIFICATES = {
CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE"
CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment"
CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth"
CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate lifetime"))
CERTIFICATE_LIFETIME = cp.getint("signature", "certificate lifetime")
CERTIFICATE_AUTHORITY_URL = cp.get("signature", "certificate url")
CERTIFICATE_CRL_URL = cp.get("signature", "revoked url")
REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation list lifetime"))
PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)])
PUSH_TOKEN = "ca"
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
PUSH_TOKEN = cp.get("push", "token")
PUSH_EVENT_SOURCE = cp.get("push", "event source")
PUSH_LONG_POLL = cp.get("push", "long poll")
PUSH_PUBLISH = cp.get("push", "publish")

24
certidude/const.py Normal file
View File

@ -0,0 +1,24 @@
import click
import os
import socket
CONFIG_DIR = os.path.expanduser("~/.certidude") if os.getuid() else "/etc/certidude"
CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_LOG_PATH = os.path.join(CONFIG_DIR, "server.log") if os.getuid() else "/var/log/certidude-server.log"
SIGNER_SOCKET_PATH = os.path.join(CONFIG_DIR, "signer.sock") if os.getuid() else "/run/certidude/signer.sock"
SIGNER_PID_PATH = os.path.join(CONFIG_DIR, "signer.pid") if os.getuid() else "/run/certidude/signer.pid"
SIGNER_LOG_PATH = os.path.join(CONFIG_DIR, "signer.log") if os.getuid() else "/var/log/certidude-signer.log"
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
if "." in FQDN:
HOSTNAME, DOMAIN = FQDN.split(".", 1)
else:
HOSTNAME, DOMAIN = FQDN, "local"
click.echo("Unable to determine domain of this computer, falling back to local")
EXTENSION_WHITELIST = set(["subjectAltName"])

View File

@ -1,13 +0,0 @@
import click
import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
if "." in FQDN:
HOSTNAME, DOMAIN = FQDN.split(".", 1)
else:
HOSTNAME, DOMAIN = FQDN, "local"
click.echo("Unable to determine domain of this computer, falling back to local")
EXTENSION_WHITELIST = set(["subjectAltName"])

View File

@ -8,7 +8,7 @@ from datetime import date, time, datetime
from OpenSSL import crypto
from certidude.auth import User
from certidude.wrappers import Request, Certificate
from urllib.parse import urlparse
from urlparse import urlparse
logger = logging.getLogger("api")
@ -23,15 +23,21 @@ def csrf_protection(func):
# For everything else assert referrer
referrer = req.headers.get("REFERER")
if referrer:
scheme, netloc, path, params, query, fragment = urlparse(referrer)
if netloc == req.host:
if ":" in netloc:
host, port = netloc.split(":", 1)
else:
host, port = netloc, None
if host == req.host:
return func(self, req, resp, *args, **kwargs)
# Kaboom!
logger.warning(u"Prevented clickbait from '%s' with user agent '%s'",
referrer or "-", req.user_agent)
raise falcon.HTTPUnauthorized("Forbidden",
raise falcon.HTTPForbidden("Forbidden",
"No suitable UA or referrer provided, cross-site scripting disabled")
return wrapped

View File

@ -6,14 +6,19 @@ import subprocess
import tempfile
from certidude import errors
from certidude.wrappers import Certificate, Request
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
from configparser import ConfigParser
from OpenSSL import crypto
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, extended_key_usage_flags=None, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, ip_address=None, dns=None, bundle=False, insecure=False):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
# Set up URL-s
request_params = set()
if autosign:
@ -22,9 +27,10 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
request_params.add("wait=forever")
# Expand ca.example.com
authority_url = "http://%s/api/certificate/" % server
request_url = "http://%s/api/request/" % server
revoked_url = "http://%s/api/revoked/" % server
scheme = "http" if insecure else "https" # TODO: Expose in CLI
authority_url = "%s://%s/api/certificate/" % (scheme, server)
request_url = "%s://%s/api/request/" % (scheme, server)
revoked_url = "%s://%s/api/revoked/" % (scheme, server)
if request_params:
request_url = request_url + "?" + "&".join(request_params)
@ -103,54 +109,62 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
# Construct private key
click.echo("Generating 4096-bit RSA key...")
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 4096)
key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
# Dump private key
key_partial = tempfile.mktemp(prefix=key_path + ".part")
os.umask(0o077)
with open(key_partial, "wb") as fh:
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
fh.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Construct CSR
csr = crypto.X509Req()
csr.set_version(2) # Corresponds to X.509v3
csr.set_pubkey(key)
csr.get_subject().CN = common_name
request = Request(csr)
# Set subject attributes
# Set subject name attributes
names = [x509.NameAttribute(NameOID.COMMON_NAME, common_name.decode("utf-8"))]
if given_name:
request.given_name = given_name
names.append(x509.NameAttribute(NameOID.GIVEN_NAME, given_name.decode("utf-8")))
if surname:
request.surname = surname
names.append(x509.NameAttribute(NameOID.SURNAME, surname.decode("utf-8")))
if org_unit:
request.organizational_unit = org_unit
names.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT, org_unit.decode("utf-8")))
# Collect subject alternative names
subject_alt_name = set()
subject_alt_names = set()
if email_address:
subject_alt_name.add("email:%s" % email_address)
subject_alt_names.add(x509.RFC822Name(email_address))
if ip_address:
subject_alt_name.add("IP:%s" % ip_address)
subject_alt_names.add("IP:%s" % ip_address)
if dns:
subject_alt_name.add("DNS:%s" % dns)
subject_alt_names.add(x509.DNSName(dns))
# Set extensions
extensions = []
if key_usage:
extensions.append(("keyUsage", key_usage, True))
if extended_key_usage:
extensions.append(("extendedKeyUsage", extended_key_usage, False))
if subject_alt_name:
extensions.append(("subjectAltName", ", ".join(subject_alt_name), False))
request.set_extensions(extensions)
# Dump CSR
# Construct CSR
csr = x509.CertificateSigningRequestBuilder(
).subject_name(x509.Name(names))
if extended_key_usage_flags:
click.echo("Adding extended key usage extension: %s" % extended_key_usage_flags)
csr = csr.add_extension(x509.ExtendedKeyUsage(
extended_key_usage_flags), critical=True)
if subject_alt_names:
click.echo("Adding subject alternative name extension: %s" % subject_alt_names)
csr = csr.add_extension(
x509.SubjectAlternativeName(subject_alt_names),
critical=False)
# Sign & dump CSR
os.umask(0o022)
with open(request_path + ".part", "w") as fh:
fh.write(request.dump())
with open(request_path + ".part", "wb") as f:
f.write(csr.sign(key, hashes.SHA256(), default_backend()).public_bytes(serialization.Encoding.PEM))
click.echo("Writing private key to: %s" % key_path)
os.rename(key_partial, key_path)
@ -160,37 +174,36 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
# We have CSR now, save the paths to client.conf so we could:
# Update CRL, renew certificate, maybe something extra?
if not os.path.exists("/etc/certidude"):
os.makedirs("/etc/certidude")
clients = ConfigParser()
if os.path.exists("/etc/certidude/client.conf"):
clients.readfp(open("/etc/certidude/client.conf"))
if clients.has_section(server):
click.echo("Section %s already exists in /etc/certidude/client.conf, not reconfiguring" % server)
else:
clients.add_section(server)
clients.set(server, "trigger", "interface up")
clients.set(server, "key_path", key_path)
clients.set(server, "request_path", request_path)
clients.set(server, "certificate_path", certificate_path)
clients.set(server, "authority_path", authority_path)
clients.set(server, "key_path", key_path)
clients.set(server, "revocations_path", revocations_path)
clients.write(open("/etc/certidude/client.conf", "w"))
click.echo("Section %s added to /etc/certidude/client.conf" % repr(server))
if os.path.exists(certificate_path):
click.echo("Found certificate: %s" % certificate_path)
# TODO: Check certificate validity, download CRL?
return
# If machine is joined to domain attempt to present machine credentials for authentication
if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"):
# Get HTTP service ticket
from configparser import ConfigParser
cp = ConfigParser(delimiters=("="))
cp.readfp(open("/etc/samba/smb.conf"))
name = cp.get("global", "netbios name")
realm = cp.get("global", "realm")
os.environ["KRB5CCNAME"]="/tmp/ca.ticket"
os.system("kinit -k %s$ -S HTTP/%s@%s -t /etc/krb5.keytab" % (name, server, realm))
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
else:
auth = None
click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url,
auth=auth,
data=open(request_path),
headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"})
# Destroy service ticket
if os.path.exists("/tmp/ca.ticket"):
os.system("kdestroy")
if submission.status_code == requests.codes.ok:
pass
if submission.status_code == requests.codes.accepted:

View File

@ -8,7 +8,7 @@ from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from urllib.parse import urlparse
from urlparse import urlparse
env = Environment(loader=PackageLoader("certidude", "templates/mail"))

View File

@ -23,12 +23,17 @@ def publish(event_type, event_data):
url,
data=event_data,
headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"})
if notification.status_code != requests.codes.created:
click.echo("Failed to submit event to push server, server responded %d, expected %d" % (
notification.status_code, requests.codes.created))
if notification.status_code == requests.codes.created:
pass # Sent to client
elif notification.status_code == requests.codes.accepted:
pass # Buffered in nchan
else:
click.echo("Failed to submit event to push server, server responded %d" % (
notification.status_code))
except requests.exceptions.ConnectionError:
click.echo("Failed to submit event to push server, connection error")
class PushLogHandler(logging.Handler):
"""
To be used with Python log handling framework for publishing log entries

View File

@ -3,7 +3,7 @@
import click
import re
import os
from urllib.parse import urlparse
from urlparse import urlparse
SCRIPTS = {}

View File

@ -1,14 +1,11 @@
import random
import pwd
import socket
import os
import asyncore
import asynchat
from certidude import constants, config
from OpenSSL import crypto
from certidude import const, config
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
@ -20,99 +17,6 @@ import random
DN_WHITELIST = NameOID.COMMON_NAME, NameOID.GIVEN_NAME, NameOID.SURNAME, \
NameOID.EMAIL_ADDRESS
SERIAL_MIN = 0x1000000000000000000000000000000000000000
SERIAL_MAX = 0xffffffffffffffffffffffffffffffffffffffff
def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None):
"""
Sign certificate signing request directly with private key assuming it's readable by the process
"""
# Initialize X.509 certificate object
cert = crypto.X509()
cert.set_version(2) # This corresponds to X.509v3
# Set public key
cert.set_pubkey(request.get_pubkey())
# Set issuer
cert.set_issuer(ca_cert.get_subject())
# Set SKID and AKID extensions
cert.add_extensions([
crypto.X509Extension(
b"subjectKeyIdentifier",
False,
b"hash",
subject = cert),
crypto.X509Extension(
b"authorityKeyIdentifier",
False,
b"keyid:always",
issuer = ca_cert),
crypto.X509Extension(
b"authorityInfoAccess",
False,
("caIssuers;URI: %s" % config.CERTIFICATE_AUTHORITY_URL).encode("ascii")),
crypto.X509Extension(
b"crlDistributionPoints",
False,
("URI: %s" % config.CERTIFICATE_CRL_URL).encode("ascii"))
])
# Copy attributes from request
cert.get_subject().CN = request.get_subject().CN
if request.get_subject().SN:
cert.get_subject().SN = request.get_subject().SN
if request.get_subject().GN:
cert.get_subject().GN = request.get_subject().GN
if request.get_subject().OU:
cert.get_subject().OU = req_subject.OU
# Copy e-mail, key usage, extended key from request
for extension in request.get_extensions():
cert.add_extensions([extension])
# TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request
# Override basic constraints if nececssary
if basic_constraints:
cert.add_extensions([
crypto.X509Extension(
b"basicConstraints",
True,
basic_constraints.encode("ascii"))])
if key_usage:
try:
cert.add_extensions([
crypto.X509Extension(
b"keyUsage",
True,
key_usage.encode("ascii"))])
except crypto.Error:
raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage)
if extended_key_usage:
cert.add_extensions([
crypto.X509Extension(
b"extendedKeyUsage",
True,
extended_key_usage.encode("ascii"))])
# Set certificate lifetime
cert.gmtime_adj_notBefore(-3600)
cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
# Generate random serial
cert.set_serial_number(random.randint(SERIAL_MIN, SERIAL_MAX))
cert.sign(private_key, 'sha512')
return cert
class SignHandler(asynchat.async_chat):
def __init__(self, sock, server):
asynchat.async_chat.__init__(self, sock=sock)
@ -162,7 +66,9 @@ class SignHandler(asynchat.async_chat):
cert = x509.CertificateBuilder(
).subject_name(subject
).serial_number(random.randint(SERIAL_MIN, SERIAL_MAX)
).serial_number(random.randint(
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff)
).issuer_name(self.server.certificate.issuer
).public_key(request.public_key()
).not_valid_before(now - timedelta(hours=1)
@ -224,32 +130,31 @@ class SignHandler(asynchat.async_chat):
def collect_incoming_data(self, data):
self.buffer.append(data)
import signal
import click
class SignServer(asyncore.dispatcher):
def __init__(self):
asyncore.dispatcher.__init__(self)
# Bind to sockets
if os.path.exists(config.SIGNER_SOCKET_PATH):
os.unlink(config.SIGNER_SOCKET_PATH)
os.umask(0o007)
if os.path.exists(const.SIGNER_SOCKET_PATH):
os.unlink(const.SIGNER_SOCKET_PATH)
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.bind(config.SIGNER_SOCKET_PATH)
self.bind(const.SIGNER_SOCKET_PATH)
self.listen(5)
# Load CA private key and certificate
click.echo("Signer reading private key from %s" % config.AUTHORITY_PRIVATE_KEY_PATH)
self.private_key = serialization.load_pem_private_key(
open(config.AUTHORITY_PRIVATE_KEY_PATH).read(),
password=None, # TODO: Ask password for private key?
backend=default_backend())
click.echo("Signer reading certificate from %s" % config.AUTHORITY_CERTIFICATE_PATH)
self.certificate = x509.load_pem_x509_certificate(
open(config.AUTHORITY_CERTIFICATE_PATH).read(),
backend=default_backend())
# Drop privileges
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody")
os.setgid(gid)
os.setuid(uid)
def handle_accept(self):
pair = self.accept()

View File

@ -14,11 +14,10 @@ backends = pam
# The accounts backend specifies how the user's given name, surname and e-mail
# address are looked up. In case of 'posix' basically 'getent passwd' is performed,
# in case of 'ldap' a search is performed on LDAP server specified in /etc/ldap/ldap.conf
# with Kerberos credential cache initialized at path specified by 'ldap gssapi credential cache'
# with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME
backend = posix
;backend = ldap
ldap gssapi credential cache = /run/certidude/krb5cc
[authorization]
# The authorization backend specifies how the users are authorized.
@ -66,6 +65,7 @@ certificate url = {{ certificate_url }}
revoked url = {{ revoked_url }}
[push]
token = {{ push_token }}
event source = {{ push_server }}/ev/%s
long poll = {{ push_server }}/lp/%s
publish = {{ push_server }}/pub?id=%s
@ -80,7 +80,6 @@ publish = {{ push_server }}/pub?id=%s
;user certificate enrollment = single allowed
user certificate enrollment = multiple allowed
private key path = {{ ca_key }}
certificate path = {{ ca_crt }}

View File

@ -0,0 +1,5 @@
#!/bin/bash
KRB5CCNAME={{ticket_path}}.part kinit -k {{name}}$ -S ldap/dc1.{{domain}}@{{realm}} -t /etc/krb5.keytab
chown certidude:certidude {{ticket_path}}.part
mv {{ticket_path}}.part {{ticket_path}}

View File

@ -1,8 +1,8 @@
server {
listen 80;
server_name {{constants.FQDN}};
rewrite ^ https://{{constants.FQDN}}$request_uri?;
server_name {{const.FQDN}};
rewrite ^ https://{{const.FQDN}}$request_uri?;
}
server {
@ -10,7 +10,7 @@ server {
add_header X-Frame-Options "DENY";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
listen 443 ssl;
server_name {{constants.FQDN}};
server_name {{const.FQDN}};
client_max_body_size 10G;
ssl_certificate {{certificate_path}};
ssl_certificate_key {{key_path}};

View File

@ -1,8 +1,4 @@
upstream certidude_api {
server unix:///run/uwsgi/app/certidude/socket;
}
server {
server_name {{ common_name }};
listen 80 default_server;
@ -11,8 +7,13 @@ server {
root {{static_path}};
location /api/ {
include uwsgi_params;
uwsgi_pass certidude_api;
proxy_pass http://127.0.0.1/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
}
{% if not push_server %}

View File

@ -1,14 +0,0 @@
client
remote {{remote}}
remote-cert-tls server
proto {{proto}}
dev tap
nobind
key {{key_path}}
cert {{certificate_path}}
ca {{authority_path}}
comp-lzo
user nobody
group nogroup
persist-tun
persist-key

View File

@ -1,22 +0,0 @@
mode server
tls-server
proto {{proto}}
port {{port}}
dev tap
local {{local}}
key {{key_path}}
cert {{certificate_path}}
ca {{authority_path}}
crl-verify {{revocations_path}}
dh {{dhparam_path}}
comp-lzo
user nobody
group nogroup
persist-tun
persist-key
ifconfig-pool-persist /tmp/openvpn-leases.txt
ifconfig {{subnet_first}} {{subnet.netmask}}
server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}}
{% for subnet in route %}
push "route {{subnet}}"
{% endfor %}

View File

@ -1,27 +0,0 @@
# /etc/ipsec.conf - strongSwan IPsec configuration file
# left/local = client
# right/remote = gateway
config setup
conn %default
ikelifetime=60m
keylife=20m
rekeymargin=3m
keyingtries=1
keyexchange=ikev2
dpdaction={{dpdaction}}
closeaction=restart
conn client-to-site
auto={{auto}}
left=%defaultroute # Use IP of default route for listening
leftsourceip=%config # Accept server suggested virtual IP as inner address for tunnel
leftcert={{certificate_path}} # Client certificate
leftid={{common_name}} # Client certificate identifier
leftfirewall=yes # Local machine may be behind NAT
right={{remote}} # Gateway IP address
rightid=%any # Allow any common name
rightsubnet=0.0.0.0/0 # Accept all subnets suggested by server

View File

@ -15,7 +15,7 @@ conn %default
keyexchange=ikev2
conn site-to-clients
auto=add
auto=ignore
right=%any # Allow connecting from any IP address
rightsourceip={{subnet}} # Serve virtual IP-s from this pool
left={{common_name}} # Gateway IP address

View File

@ -0,0 +1,15 @@
[Unit]
Description=Certidude server
After=network.target
[Service]
Environment=PYTHON_EGG_CACHE=/tmp/.cache
Environment=KRB5_KTNAME={{kerberos_keytab}}
PIDFile=/run/certidude/server.pid
ExecStart={{ certidude_path }} serve {% if listen} -l {{listen}}{% endif %}{% if port %} -p {{port}}{% endif %}
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target

View File

@ -1,19 +0,0 @@
[uwsgi]
exec-as-root = /usr/local/bin/certidude signer spawn
master = true
processes = 1
vacuum = true
uid = {{username}}
gid = {{username}}
plugins = python27
chdir = /tmp
module = certidude.wsgi
callable = app
chmod-socket = 660
chown-socket = {{username}}:www-data
buffer-size = 32768
env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8
env = KRB5_KTNAME={{kerberos_keytab}}
env = KRB5CCNAME=/run/certidude/krb5cc

View File

@ -5,7 +5,7 @@ import ldap
import ldap.sasl
import os
import pwd
from certidude import constants, config
from certidude import const, config
class User(object):
def __init__(self, username, mail, given_name="", surname=""):
@ -46,7 +46,7 @@ class PosixUserManager(object):
_, _, _, _, gecos, _, _ = pwd.getpwnam(username)
gecos = gecos.decode("utf-8").split(",")
full_name = gecos[0]
mail = username + "@" + constants.DOMAIN
mail = username + "@" + const.DOMAIN
if full_name and " " in full_name:
given_name, surname = full_name.split(" ", 1)
return User(username, mail, given_name, surname)
@ -67,8 +67,8 @@ class DirectoryConnection(object):
def __enter__(self):
# TODO: Implement simple bind
if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE):
raise ValueError("Ticket cache not initialized, unable to "
"authenticate with computer account against LDAP server!")
raise ValueError("Ticket cache at %s not initialized, unable to "
"authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE)
os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE
for server in config.LDAP_SERVERS:
self.conn = ldap.initialize(server)
@ -106,7 +106,7 @@ class ActiveDirectoryUserManager(object):
else:
given_name, surname = cn, ""
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,)
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + const.DOMAIN,)
return User(username.decode("utf-8"), mail.decode("utf-8"),
given_name.decode("utf-8"), surname.decode("utf-8"))
raise User.DoesNotExist("User %s does not exist" % username)
@ -121,7 +121,7 @@ class ActiveDirectoryUserManager(object):
continue
username, = entry.get("sAMAccountName")
cn, = entry.get("cn")
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,)
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + const.DOMAIN,)
if entry.get("givenName") and entry.get("sn"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")

View File

@ -3,7 +3,7 @@ import hashlib
import re
import click
import io
from certidude import constants
from certidude import const
from OpenSSL import crypto
from datetime import datetime
@ -228,7 +228,7 @@ class Request(CertificateBase):
@property
def signable(self):
for key, value, data in self.extensions:
if key not in constants.EXTENSION_WHITELIST:
if key not in const.EXTENSION_WHITELIST:
return False
return True

View File

@ -1,12 +0,0 @@
"""
certidude.wsgi
~~~~~~~~~~~~~~
Certidude web app factory for WSGI-compatible web servers
"""
import os
from certidude.api import certidude_app
# TODO: set up /run/certidude/api paths and permissions
app = certidude_app()

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
from certidude.cli import entry_point

View File

@ -1,21 +1,25 @@
cffi==1.2.1
click==5.1
configparser
cryptography==1.0
falcon==0.3.0
humanize==0.5.1
idna==2.0
ipaddress==1.0.16
ipsecparse==0.1.0
Jinja2==2.8
Markdown==2.6.5
pyasn1==0.1.8
pycrypto==2.6.1
pykerberos==1.1.8
pyOpenSSL==0.15.1
python-ldap==2.4.10
python-mimeparse==0.1.4
requests==2.2.1
setproctitle==1.1.9
simplepam==0.1.5
six==1.9.0
Markdown==2.6.6
MarkupSafe==0.23
argparse==1.2.1
certifi==2016.2.28
cffi==1.7.0
click==6.6
configparser==3.5.0
cryptography==1.4
enum34==1.1.6
falcon==1.0.0
humanize==0.5.1
ipaddress==1.0.16
Markdown==2.6.6
ndg-httpsclient==0.4.2
pyOpenSSL==16.0.0
pyasn1==0.1.9
pycparser==2.14
python-ldap==2.4.25
python-mimeparse==1.5.2
requests==2.10.0
setproctitle==1.1.10
six==1.10.0
urllib3==1.16
wsgiref==0.1.2

View File

@ -10,7 +10,7 @@ setup(
author_email = "lauri.vosandi@gmail.com",
description = "Certidude is a novel X.509 Certificate Authority management tool aiming to support PKCS#11 and in far future WebCrypto.",
license = "MIT",
keywords = "falcon http jinja2 x509 pkcs11 webcrypto",
keywords = "falcon http jinja2 x509 pkcs11 webcrypto kerberos ldap",
url = "http://github.com/laurivosandi/certidude",
packages=[
"certidude",
@ -23,13 +23,9 @@ setup(
"falcon",
"jinja2",
"pyopenssl",
"pycountry",
"humanize",
"pycrypto",
"cryptography",
"markupsafe",
"ldap3",
"pykerberos",
],
scripts=[
"misc/certidude"