1
0
mirror of https://github.com/laurivosandi/certidude synced 2025-09-09 23:11:12 +00:00

Refactor users, add OpenVPN and mailing support

* Add abstraction for user objects
* Mail authority admins about pending, revoked and signed certificates
* Add NetworkManager's OpenVPN plugin support
* Improve CRL support
* Refactor CSRF protection
* Update documentation
This commit is contained in:
2016-03-27 23:38:14 +03:00
parent 811e6dbb08
commit 925bc0ef9a
25 changed files with 695 additions and 564 deletions

View File

@@ -9,6 +9,7 @@ from datetime import datetime
from time import sleep
from certidude import authority, mailer
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
@@ -33,34 +34,16 @@ class CertificateAuthorityResource(object):
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
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=ca.crt")
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
constants.HOSTNAME.encode("ascii"))
class SessionResource(object):
@csrf_protection
@serialize
@login_required
@authorize_admin
@event_source
def on_get(self, req, resp):
if config.ACCOUNTS_BACKEND == "ldap":
import ldap
ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*")
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE,
ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"])
for dn,entry in r:
cn, = entry.get("cn")
break
else:
raise ValueError("Failed to look up group %s in LDAP" % repr(group_name))
admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")])
elif config.ACCOUNTS_BACKEND == "posix":
import grp
_, _, gid, members = grp.getgrnam(config.ADMINS_GROUP)
admins = dict([(j, j) for j in members])
else:
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
return dict(
user = dict(
@@ -72,12 +55,6 @@ class SessionResource(object):
request_submission_allowed = sum( # Dirty hack!
[req.context.get("remote_addr") in j
for j in config.REQUEST_SUBNETS]),
user_subnets = config.USER_SUBNETS,
autosign_subnets = config.AUTOSIGN_SUBNETS,
request_subnets = config.REQUEST_SUBNETS,
admin_subnets=config.ADMIN_SUBNETS,
admin_users = admins,
#admin_users=config.ADMIN_USERS,
authority = dict(
outbox = config.OUTBOX,
certificate = authority.certificate,
@@ -85,7 +62,12 @@ class SessionResource(object):
requests=authority.list_requests(),
signed=authority.list_signed(),
revoked=authority.list_revoked(),
) if config.ADMINS_GROUP in req.context.get("groups") else None,
admin_users = User.objects.filter_admins(),
user_subnets = config.USER_SUBNETS,
autosign_subnets = config.AUTOSIGN_SUBNETS,
request_subnets = config.REQUEST_SUBNETS,
admin_subnets=config.ADMIN_SUBNETS,
) if req.context.get("user").is_admin() else None,
features=dict(
tagging=config.TAGGING_BACKEND,
leases=False, #config.LEASES_BACKEND,
@@ -124,7 +106,7 @@ class BundleResource(object):
common_name = req.context["user"].mail
logger.info("Signing bundle %s for %s", common_name, req.context.get("user"))
resp.set_header("Content-Type", "application/x-pkcs12")
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name)
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user"))
@@ -132,7 +114,6 @@ class BundleResource(object):
import ipaddress
class NormalizeMiddleware(object):
@csrf_protection
def process_request(self, req, resp, *args):
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"))

View File

@@ -6,7 +6,7 @@ import ipaddress
import os
from certidude import config, authority, helpers, push, errors
from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import serialize
from certidude.decorators import serialize, csrf_protection
from certidude.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types
@@ -19,6 +19,7 @@ class RequestListResource(object):
def on_get(self, req, resp):
return authority.list_requests()
@login_optional
@whitelist_subnets(config.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10")
@@ -53,7 +54,7 @@ class RequestListResource(object):
# Process automatic signing if the IP address is whitelisted and autosigning was requested
if req.get_param_as_bool("autosign"):
for subnet in config.AUTOSIGN_SUBNETS:
if subnet.overlaps(req.context.get("remote_addr")):
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr).dump()
@@ -103,6 +104,8 @@ class RequestDetailResource(object):
csr.common_name, req.context.get("remote_addr"))
return csr
@csrf_protection
@login_required
@authorize_admin
def on_patch(self, req, resp, cn):
@@ -118,6 +121,8 @@ class RequestDetailResource(object):
logger.info("Signing request %s signed by %s from %s", csr.common_name,
req.context.get("user"), req.context.get("remote_addr"))
@csrf_protection
@login_required
@authorize_admin
def on_delete(self, req, resp, cn):

View File

@@ -3,7 +3,7 @@ import falcon
import logging
from certidude import authority
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api")
@@ -24,20 +24,21 @@ class SignedCertificateDetailResource(object):
try:
cert = authority.get_signed(cn)
except EnvironmentError:
logger.warning("Failed to serve non-existant certificate %s to %s",
logger.warning(u"Failed to serve non-existant certificate %s to %s",
cn, req.context.get("remote_addr"))
resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound()
else:
logger.debug("Served certificate %s to %s",
logger.debug(u"Served certificate %s to %s",
cn, req.context.get("remote_addr"))
return cert
@csrf_protection
@login_required
@authorize_admin
def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s",
logger.info(u"Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr"))
authority.revoke_certificate(cn)

View File

@@ -3,7 +3,7 @@ import falcon
import logging
from certidude.relational import RelationalMixin
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize
from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api")
@@ -17,6 +17,7 @@ class TagResource(RelationalMixin):
return self.iterfetch("select * from tag")
@csrf_protection
@serialize
@login_required
@authorize_admin
@@ -51,6 +52,7 @@ class TagDetailResource(RelationalMixin):
raise falcon.HTTPNotFound()
@csrf_protection
@serialize
@login_required
@authorize_admin
@@ -63,6 +65,7 @@ class TagDetailResource(RelationalMixin):
push.publish("tag-updated", identifier)
@csrf_protection
@serialize
@login_required
@authorize_admin

View File

@@ -6,14 +6,13 @@ import logging
import os
import re
import socket
from certidude.user import User
from certidude.firewall import whitelist_subnets
from certidude import config, constants
logger = logging.getLogger("api")
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
if "kerberos" in config.AUTHENTICATION_BACKENDS:
ktname = os.getenv("KRB5_KTNAME")
if not ktname:
@@ -24,139 +23,13 @@ if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
exit(248)
try:
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN)
except kerberos.KrbError as exc:
click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True)
click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (
constants.FQDN, exc), err=True)
exit(249)
else:
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
class User(object):
def __init__(self, name):
if "@" in name:
self.mail = name
self.name, self.domain = name.split("@")
else:
self.mail = None
self.name, self.domain = name, None
self.given_name, self.surname = None, None
def __repr__(self):
if self.given_name and self.surname:
return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
else:
return self.mail
def member_of(group_name):
"""
Check if requesting user is member of an UNIX group
"""
def wrapper(func):
def posix_check_group_membership(resource, req, resp, *args, **kwargs):
import grp
_, _, gid, members = grp.getgrnam(group_name)
if req.context.get("user").name not in members:
logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name)
raise falcon.HTTPForbidden("Forbidden", "User not member of designated group")
req.context.get("groups").add(group_name)
return func(resource, req, resp, *args, **kwargs)
def ldap_check_group_membership(resource, req, resp, *args, **kwargs):
import ldap
ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn)
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"),
["member"])
for dn,entry in r:
if not dn: continue
logger.debug("User %s is member of group %s" % (
req.context.get("user"), repr(group_name)))
req.context.get("groups").add(group_name)
break
else:
raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name))
return func(resource, req, resp, *args, **kwargs)
if config.AUTHORIZATION_BACKEND == "ldap":
return ldap_check_group_membership
elif config.AUTHORIZATION_BACKEND == "posix":
return posix_check_group_membership
else:
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
return wrapper
def account_info(func):
# TODO: Use Privilege Account Certificate for Kerberos
def posix_account_info(resource, req, resp, *args, **kwargs):
import pwd
_, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name)
gecos = gecos.decode("utf-8").split(",")
full_name = gecos[0]
if full_name and " " in full_name:
req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1)
req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN
return func(resource, req, resp, *args, **kwargs)
def ldap_account_info(resource, req, resp, *args, **kwargs):
import ldap
import ldap.sasl
if "ldap_conn" not in req.context:
for server in config.LDAP_SERVERS:
conn = ldap.initialize(server)
conn.set_option(ldap.OPT_REFERRALS, 0)
if os.path.exists("/etc/krb5.keytab"):
ticket_cache = os.getenv("KRB5CCNAME")
if not ticket_cache:
raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!")
click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache))
conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
else:
raise NotImplementedError("LDAP simple bind not supported, use Kerberos")
req.context["ldap_conn"] = conn
break
else:
raise ValueError("No LDAP servers!")
ft = config.LDAP_USER_FILTER % req.context.get("user").name
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft,
["cn", "givenname", "sn", "mail", "userPrincipalName"])
for dn, entry in r:
if not dn: continue
if entry.get("givenname") and entry.get("sn"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")
req.context["user"].given_name = given_name.decode("utf-8")
req.context["user"].surname = surname.decode("utf-8")
else:
cn, = entry.get("cn")
if " " in cn:
req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1)
req.context["user"].dn = dn.decode("utf-8")
req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,)
retval = func(resource, req, resp, *args, **kwargs)
req.context.get("ldap_conn").unbind_s()
return retval
else:
raise ValueError("Failed to look up %s in LDAP" % req.context.get("user"))
if config.ACCOUNTS_BACKEND == "ldap":
return ldap_account_info
elif config.ACCOUNTS_BACKEND == "posix":
return posix_account_info
else:
raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND)
click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.FQDN)
def authenticate(optional=False):
@@ -167,7 +40,7 @@ def authenticate(optional=False):
if not req.auth:
resp.append_header("WWW-Authenticate", "Negotiate")
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
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?")
@@ -175,7 +48,7 @@ def authenticate(optional=False):
token = ''.join(req.auth.split()[1:])
try:
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN)
except kerberos.GSSError as ex:
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
@@ -185,34 +58,46 @@ def authenticate(optional=False):
result = kerberos.authGSSServerStep(context, token)
except kerberos.GSSError as ex:
kerberos.authGSSServerClean(context)
# TODO: logger.error
logger.error(u"Kerberos authentication failed from %s. "
"Bad credentials: %s (%d)",
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]))
except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context)
# TODO: logger.error
logger.error(u"Kerberos authentication failed from %s. "
"Bad credentials: %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],))
user = kerberos.authGSSServerUserName(context)
req.context["user"] = User(user)
req.context["groups"] = set()
req.context["user"] = User.objects.get(user)
try:
kerberos.authGSSServerClean(context)
except kerberos.GSSError as ex:
# TODO: logger.error
logger.error(u"Kerberos authentication failed for user %s from %s. "
"Authentication system failure: %s (%d)",
user, req.context.get("remote_addr"),
ex.args[0][0], ex.args[0][1])
raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0]))
if result == kerberos.AUTH_GSS_COMPLETE:
logger.debug("Succesfully authenticated user %s for %s from %s",
logger.debug(u"Succesfully authenticated user %s for %s from %s",
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
return account_info(func)(resource, req, resp, *args, **kwargs)
return func(resource, req, resp, *args, **kwargs)
elif result == kerberos.AUTH_GSS_CONTINUE:
# TODO: logger.error
logger.error(u"Kerberos authentication failed for user %s from %s. "
"Unauthorized, tried GSSAPI.",
user, req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
else:
# TODO: logger.error
logger.error(u"Kerberos authentication failed for user %s from %s. "
"Forbidden, tried GSSAPI.",
user, req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
@@ -238,27 +123,26 @@ def authenticate(optional=False):
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).split(":", 1)
if "ldap_conn" not in req.context:
for server in config.LDAP_SERVERS:
click.echo("Connecting to %s as %s" % (server, user))
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)
except ldap.LDAPError, e:
resp.append_header("WWW-Authenticate", "Basic")
logger.debug("Failed to authenticate with user '%s'", user)
raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
for server in config.LDAP_SERVERS:
click.echo("Connecting to %s as %s" % (server, user))
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)
except ldap.LDAPError, e:
resp.append_header("WWW-Authenticate", "Basic")
logger.critical("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)
req.context["ldap_conn"] = conn
break
else:
raise ValueError("No LDAP servers!")
req.context["ldap_conn"] = conn
break
else:
raise ValueError("No LDAP servers!")
req.context["user"] = User(user)
req.context["groups"] = set()
return account_info(func)(resource, req, resp, *args, **kwargs)
req.context["user"] = User.objects.get(user)
return func(resource, req, resp, *args, **kwargs)
def pam_authenticate(resource, req, resp, *args, **kwargs):
@@ -282,11 +166,12 @@ def authenticate(optional=False):
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
logger.critical("Basic authentication failed for user %s from %s",
repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
req.context["user"] = User(user)
req.context["groups"] = set()
return account_info(func)(resource, req, resp, *args, **kwargs)
req.context["user"] = User.objects.get(user)
return func(resource, req, resp, *args, **kwargs)
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
return kerberos_authenticate
@@ -302,14 +187,11 @@ def authenticate(optional=False):
def login_required(func):
return authenticate()(func)
def login_optional(func):
return authenticate(optional=True)(func)
def authorize_admin(func):
def whitelist_authorize(resource, req, resp, *args, **kwargs):
def whitelist_authorize_admin(resource, req, resp, *args, **kwargs):
# Check for username whitelist
if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST:
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted",
@@ -317,8 +199,13 @@ def authorize_admin(func):
raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
return func(resource, req, resp, *args, **kwargs)
if config.AUTHORIZATION_BACKEND == "whitelist":
return whitelist_authorize
else:
return member_of(config.ADMINS_GROUP)(func)
def authorize_admin(resource, req, resp, *args, **kwargs):
if req.context.get("user").is_admin():
req.context["admin_authorized"] = True
return func(resource, req, resp, *args, **kwargs)
logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name)
raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations")
if config.AUTHORIZATION_BACKEND == "whitelist":
return whitelist_authorize_admin
return authorize_admin

View File

@@ -26,12 +26,12 @@ def publish_certificate(func):
cert = func(csr, *args, **kwargs)
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
if cert.email_address:
mailer.send(
"%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address),
"certificate-signed.md",
attachments=(cert,),
certificate=cert)
mailer.send(
"certificate-signed.md",
to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if
cert.given_name and cert.surname else cert.email_address,
attachments=(cert,),
certificate=cert)
if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % csr.fingerprint()
@@ -85,7 +85,9 @@ def store_request(buf, overwrite=False):
fh.write(buf)
os.rename(request_path + ".part", request_path)
return Request(open(request_path))
req = Request(open(request_path))
mailer.send("request-stored.md", attachments=(req,), request=req)
return req
def signer_exec(cmd, *bits):
@@ -110,6 +112,7 @@ def revoke_certificate(common_name):
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
os.rename(cert.path, revoked_filename)
push.publish("certificate-revoked", cert.common_name)
mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert)
def list_requests(directory=config.REQUESTS_DIR):
@@ -184,7 +187,7 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
if owner.surname:
csr.get_subject().SN = owner.surname
csr.add_extensions([
crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)])
crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail.encode("ascii"))])
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)

View File

@@ -15,6 +15,7 @@ import subprocess
import sys
from configparser import ConfigParser
from certidude import constants
from certidude.helpers import certidude_request_certificate
from certidude.common import expand_paths, ip_address, ip_network
from datetime import datetime
from humanize import naturaltime
@@ -63,8 +64,6 @@ if os.getuid() >= 1000:
@click.command("spawn", help="Run processes for requesting certificates and configuring services")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_request_spawn(fork):
from certidude.helpers import certidude_request_certificate
clients = ConfigParser()
clients.readfp(open("/etc/certidude/client.conf"))
@@ -80,7 +79,7 @@ def certidude_request_spawn(fork):
os.makedirs(run_dir)
for server in clients.sections():
if clients.get(server, "managed") != "true":
if clients.get(server, "trigger") != "interface up":
continue
pid_path = os.path.join(run_dir, server + ".pid")
@@ -115,6 +114,7 @@ def certidude_request_spawn(fork):
clients.get(server, "request_path"),
clients.get(server, "certificate_path"),
clients.get(server, "authority_path"),
clients.get(server, "revocations_path"),
socket.gethostname(),
None,
autosign=True,
@@ -133,9 +133,47 @@ def certidude_request_spawn(fork):
csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
# Intranet HTTPS handled by PKCS#12 bundle generation,
# so it will not be implemented here
if services.get(endpoint, "service") == "network-manager/openvpn":
config = ConfigParser()
config.add_section("connection")
config.add_section("vpn")
config.add_section("ipv4")
config.add_section("ipv6")
config.set("connection", "id", endpoint)
config.set("connection", "uuid", uuid)
config.set("connection", "type", "vpn")
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn")
config.set("vpn", "connection-type", "tls")
config.set("vpn", "comp-lzo", "yes")
config.set("vpn", "cert-pass-flags", "0")
config.set("vpn", "tap-dev", "yes")
config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
config.set("vpn", "remote", services.get(endpoint, "remote"))
config.set("vpn", "key", clients.get(server, "key_path"))
config.set("vpn", "cert", clients.get(server, "certificate_path"))
config.set("vpn", "ca", clients.get(server, "authority_path"))
config.set("ipv6", "method", "auto")
config.set("ipv4", "method", "auto")
config.set("ipv4", "never-default", "true")
# Prevent creation of files with liberal permissions
os.umask(0o177)
# Write keyfile
with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as configfile:
config.write(configfile)
continue
# Set up IPsec via NetworkManager
if services.get(endpoint, "service") == "network-manager/strongswan":
config = ConfigParser()
config.add_section("connection")
config.add_section("vpn")
@@ -146,14 +184,14 @@ def certidude_request_spawn(fork):
config.set("connection", "type", "vpn")
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
config.set("vpn", "userkey", clients.get(server, "key_path"))
config.set("vpn", "usercert", clients.get(server, "certificate_path"))
config.set("vpn", "encap", "no")
config.set("vpn", "address", services.get(endpoint, "remote"))
config.set("vpn", "virtual", "yes")
config.set("vpn", "method", "key")
config.set("vpn", "certificate", clients.get(server, "authority_path"))
config.set("vpn", "ipcomp", "no")
config.set("vpn", "address", services.get(endpoint, "remote"))
config.set("vpn", "userkey", clients.get(server, "key_path"))
config.set("vpn", "usercert", clients.get(server, "certificate_path"))
config.set("vpn", "certificate", clients.get(server, "authority_path"))
config.set("ipv4", "method", "auto")
@@ -203,9 +241,7 @@ def certidude_request_spawn(fork):
os.system("ipsec start")
continue
# TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS, <insert awesomeness here>
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
os.unlink(pid_path)
@@ -284,9 +320,8 @@ def certidude_signer_spawn(kill, no_interaction):
asyncore.loop()
@click.command("client", help="Setup X.509 certificates for application")
@click.argument("url") #, help="Certidude authority endpoint URL")
@click.argument("server")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, '%s' by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@@ -301,18 +336,18 @@ def certidude_signer_spawn(kill, no_interaction):
@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr by default" % HOSTNAME)
@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % HOSTNAME)
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
def certidude_setup_client(quiet, **kwargs):
from certidude.helpers import certidude_request_certificate
return certidude_request_certificate(**kwargs)
@click.command("server", help="Set up OpenVPN server")
@click.argument("url")
@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", default="127.0.0.1", help="OpenVPN listening address, defaults to 127.0.0.1")
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
@@ -321,15 +356,15 @@ def certidude_setup_client(quiet, **kwargs):
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to --directory by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
def certidude_setup_openvpn_server(url, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port):
def certidude_setup_openvpn_server(server, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, local, proto, port):
# TODO: Intelligent way of getting last IP address in the subnet
from certidude.helpers import certidude_request_certificate
subnet_first = None
subnet_last = None
subnet_second = None
@@ -346,15 +381,9 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
retval = certidude_request_certificate(
url,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path, revocations_path,
common_name, org_unit, email_address,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
wait=True)
@@ -378,7 +407,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("url")
@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--tls-config",
@@ -392,14 +421,14 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
@expand_paths()
def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client):
def certidude_setup_nginx(server, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
# TODO: Intelligent way of getting last IP address in the subnet
from certidude.helpers import certidude_request_certificate
if not os.path.exists(certificate_path):
click.echo("As HTTPS server certificate needs specific key usage extensions please")
@@ -407,8 +436,8 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d
click.echo()
click.echo(" certidude sign %s" % common_name)
click.echo()
retval = certidude_request_certificate(url, key_path, request_path,
certificate_path, authority_path, common_name, org_unit,
retval = certidude_request_certificate(server, key_path, request_path,
certificate_path, authority_path, revocations_path, common_name, org_unit,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
dns = constants.FQDN, wait=True, bundle=True)
@@ -446,33 +475,28 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d
@click.command("client", help="Set up OpenVPN client")
@click.argument("url")
@click.argument("server")
@click.argument("remote")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
@click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
@click.option("--key-path", "-k", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME)
@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME)
@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
def certidude_setup_openvpn_client(server, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, proto, remote):
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path, revocations_path,
common_name, org_unit, email_address,
wait=True)
if retval:
@@ -490,7 +514,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.command("server", help="Set up strongSwan server")
@click.argument("url")
@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate")
@@ -511,8 +535,9 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default")
@expand_paths()
def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn):
def certidude_setup_strongswan_server(server, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, local, fqdn):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
if not local:
@@ -523,19 +548,13 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
click.echo()
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path, revocations_path,
common_name, org_unit, email_address,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
ip_address=local,
dns=fqdn,
wait=True)
@@ -555,7 +574,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.command("client", help="Set up strongSwan client")
@click.argument("url")
@click.argument("server")
@click.argument("remote")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@@ -581,18 +600,12 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@click.option("--revocations-path", "-crl", default="crls/ca.pemf", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
def certidude_setup_strongswan_client(server, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path,
common_name, org_unit, email_address,
wait=True)
if retval:
@@ -612,8 +625,8 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("url")
@click.argument("remote")
@click.argument("server") # Certidude server
@click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@@ -622,63 +635,71 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default")
@expand_paths()
def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate(
url,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
def certidude_setup_strongswan_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote):
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path, revocations_path,
common_name, org_unit, email_address,
wait=True)
if retval:
return retval
csummer = hashlib.sha1()
csummer.update(remote.encode("ascii"))
csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
services = ConfigParser()
if os.path.exists("/etc/certidude/services.conf"):
services.readfp(open("/etc/certidude/services.conf"))
config = ConfigParser()
config.add_section("connection")
config.add_section("vpn")
config.add_section("ipv4")
endpoint = "IPSec to %s" % remote
config.set("connection", "id", remote)
config.set("connection", "uuid", uuid)
config.set("connection", "type", "vpn")
config.set("connection", "autoconnect", "true")
if services.has_section(endpoint):
click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint)
else:
click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)
services.add_section(endpoint)
services.set(endpoint, "authority", server)
services.set(endpoint, "remote", remote)
services.set(endpoint, "service", "network-manager/strongswan")
services.write(open("/etc/certidude/services.conf", "w"))
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
config.set("vpn", "userkey", key_path)
config.set("vpn", "usercert", certificate_path)
config.set("vpn", "encap", "no")
config.set("vpn", "address", remote)
config.set("vpn", "virtual", "yes")
config.set("vpn", "method", "key")
config.set("vpn", "certificate", authority_path)
config.set("vpn", "ipcomp", "no")
config.set("ipv4", "method", "auto")
@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
@click.argument("server") # Certidude server
@click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate path, ca.crt relative to -d by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
@expand_paths()
def certidude_setup_openvpn_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote):
retval = certidude_request_certificate(server,
key_path, request_path, certificate_path, authority_path, revocations_path,
common_name, org_unit, email_address,
wait=True)
# Prevent creation of files with liberal permissions
os.umask(0o277)
if retval:
return retval
# Write keyfile
with open(os.path.join("/etc/NetworkManager/system-connections", remote), "w") as configfile:
config.write(configfile)
services = ConfigParser()
if os.path.exists("/etc/certidude/services.conf"):
services.readfp(open("/etc/certidude/services.conf"))
# TODO: Avoid race condition here
sleep(3)
# Tell NetworkManager to bring up the VPN connection
subprocess.call(("nmcli", "c", "up", "uuid", uuid))
endpoint = "OpenVPN to %s" % remote
if services.has_section(endpoint):
click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint)
else:
click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)
services.add_section(endpoint)
services.set(endpoint, "authority", server)
services.set(endpoint, "remote", remote)
services.set(endpoint, "service", "network-manager/openvpn")
services.write(open("/etc/certidude/services.conf", "w"))
@click.command("production", help="Set up nginx, uwsgi and cron")
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@@ -761,7 +782,8 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
@click.option("--push-server", default="", help="Streaming nginx push server")
@click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA")
@click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/ by default")
def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address):
@click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN)
def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address, outbox):
# Make sure common_name is valid
if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name):
@@ -1155,6 +1177,7 @@ certidude_setup_strongswan.add_command(certidude_setup_strongswan_client)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
certidude_setup.add_command(certidude_setup_authority)
certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan)

View File

@@ -57,7 +57,6 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend")
LEASES_BACKEND = cp.get("leases", "backend")
@@ -68,18 +67,16 @@ if "whitelist" == AUTHORIZATION_BACKEND:
ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
elif "posix" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "posix user group")
ADMINS_GROUP = cp.get("authorization", "posix admin group")
ADMIN_GROUP = cp.get("authorization", "posix admin group")
elif "ldap" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "ldap user group")
ADMINS_GROUP = cp.get("authorization", "ldap admin group")
LDAP_GSSAPI_CRED_CACHE = cp.get("authorization", "ldap gssapi credential cache")
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter")
if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'")
if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'")
else:
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter")
LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter")
LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter")
for line in open("/etc/ldap/ldap.conf"):
line = line.strip().lower()
if "#" in line:
@@ -92,6 +89,5 @@ for line in open("/etc/ldap/ldap.conf"):
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
elif key == "base":
LDAP_BASE = value
else:
click.echo("No LDAP servers specified in /etc/ldap/ldap.conf")
# TODO: Check if we don't have base or servers

View File

@@ -1,4 +1,5 @@
import click
import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]

View File

@@ -6,6 +6,7 @@ import re
import types
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
@@ -76,6 +77,9 @@ class MyEncoder(json.JSONEncoder):
if isinstance(obj, Certificate):
return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \
if hasattr(obj, key) and getattr(obj, key)])
if isinstance(obj, User):
return dict(name=obj.name, given_name=obj.given_name,
surname=obj.surname, mail=obj.mail)
if hasattr(obj, "serialize"):
return obj.serialize()
return json.JSONEncoder.default(self, obj)
@@ -95,16 +99,20 @@ def serialize(func):
resp.set_header("Content-Type", "application/json")
resp.set_header("Content-Disposition", "inline")
resp.body = json.dumps(r, cls=MyEncoder)
elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
resp.set_header("Content-Type", r.content_type)
resp.set_header("Content-Disposition",
("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
resp.body = r.dump()
else:
logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept))
elif hasattr(r, "content_type"):
logger.debug("Client did not accept application/json or %s, "
"client expected %s", r.content_type, req.accept)
raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json or %s" % r.content_type)
else:
logger.debug("Client did not accept application/json, client expected %s", req.accept)
raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json")
return r
return wrapped

View File

@@ -2,11 +2,14 @@
import click
import os
import requests
import subprocess
import tempfile
from certidude import errors
from certidude.wrappers import Certificate, Request
from configparser import ConfigParser
from OpenSSL import crypto
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_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, 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):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
@@ -18,38 +21,80 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
if wait:
request_params.add("wait=forever")
# Expand ca.example.com to http://ca.example.com/api/
if not url.endswith("/"):
url += "/api/"
if "//" not in url:
url = "http://" + url
authority_url = url + "certificate"
request_url = url + "request"
# 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
if request_params:
request_url = request_url + "?" + "&".join(request_params)
if os.path.exists(certificate_path):
click.echo("Found certificate: %s" % certificate_path)
# TODO: Check certificate validity, download CRL?
return
if os.path.exists(authority_path):
click.echo("Found CA certificate in: %s" % authority_path)
click.echo("Found authority certificate in: %s" % authority_path)
else:
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
click.echo("Attempting to fetch authority certificate from %s" % authority_url)
try:
r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % r.text)
with open(authority_path + ".part", "w") as oh:
authority_partial = tempfile.mktemp(prefix=authority_path + ".part")
with open(authority_partial, "w") as oh:
oh.write(r.text)
click.echo("Writing CA certificate to: %s" % authority_path)
os.rename(authority_path + ".part", authority_path)
click.echo("Writing authority certificate to: %s" % authority_path)
os.rename(authority_partial, authority_path)
# Fetch certificate revocation list
r = requests.get(revoked_url, stream=True)
click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path))
revocations_partial = tempfile.mktemp(prefix=revocations_path + ".part")
with open(revocations_partial, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
if subprocess.call(("openssl", "crl", "-CAfile", authority_path, "-in", revocations_partial, "-noout")):
raise ValueError("Failed to verify CRL in %s" % revocations_partial)
else:
# TODO: Check monotonically increasing CRL number
click.echo("Certificate revocation list passed verification")
os.rename(revocations_partial, revocations_path)
# Check if we have been inserted into CRL
if os.path.exists(certificate_path):
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read())
revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read())
for revocation in revocation_list.get_revoked():
if int(revocation.get_serial(), 16) == cert.get_serial_number():
if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL'
# TODO: Disable service for time being
click.echo("Certificate put on hold, doing nothing for now")
break
# Disable the client if operation has been ceased or
# the certificate has been superseded by other
if revocation.get_reason() in ("Cessation Of Operation", "Superseded"):
if os.path.exists("/etc/certidude/client.conf"):
clients.readfp(open("/etc/certidude/client.conf"))
if clients.has_section(server):
clients.set(server, "trigger", "operation ceased")
clients.write(open("/etc/certidude/client.conf", "w"))
click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf")
# TODO: Disable related services
if revocation.get_reason() in ("CA Compromise", "AA Compromise"):
if os.path.exists(authority_path):
os.remove(key_path)
click.echo("Certificate has been revoked, wiping keys and certificates!")
if os.path.exists(key_path):
os.remove(key_path)
if os.path.exists(request_path):
os.remove(request_path)
if os.path.exists(certificate_path):
os.remove(certificate_path)
break
else:
click.echo("Certificate does not seem to be revoked. Good!")
try:
request = Request(open(request_path))
@@ -62,8 +107,9 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
key.generate_key(crypto.TYPE_RSA, 4096)
# Dump private key
key_partial = tempfile.mktemp(prefix=key_path + ".part")
os.umask(0o077)
with open(key_path + ".part", "wb") as fh:
with open(key_partial, "wb") as fh:
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
# Construct CSR
@@ -107,10 +153,38 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
fh.write(request.dump())
click.echo("Writing private key to: %s" % key_path)
os.rename(key_path + ".part", key_path)
os.rename(key_partial, key_path)
click.echo("Writing certificate signing request to: %s" % request_path)
os.rename(request_path + ".part", request_path)
# 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
click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url,

View File

@@ -1,6 +1,8 @@
import click
import os
import smtplib
from certidude.user import User
from markdown import markdown
from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart
@@ -10,14 +12,18 @@ from urllib.parse import urlparse
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
def send(recipients, template, attachments=(), **context):
def send(template, to=None, attachments=(), **context):
from certidude import authority, config
if not config.OUTBOX:
# Mailbox disabled, don't send e-mail
return
if not recipients:
raise ValueError("No e-mail recipients specified!")
recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()])
if to:
recipients = to + u", " + recipients
click.echo("Sending e-mail %s to %s" % (template, recipients))
scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
scheme = scheme.lower()

View File

@@ -485,14 +485,14 @@ output += "\n E-mail disabled\n";
;
}
output += "</p>\n\n<p>Authenticated users allowed from:\n\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) {
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"))) {
output += "\n anywhere\n </p>\n";
;
}
else {
output += "\n </p>\n <ul>\n ";
frame = frame.push();
var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets");
var t_3 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets");
if(t_3) {var t_2 = t_3.length;
for(var t_1=0; t_1 < t_3.length; t_1++) {
var t_4 = t_3[t_1];
@@ -515,14 +515,14 @@ output += "\n </ul>\n";
;
}
output += "\n\n\n<p>Request submission is allowed from:\n\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) {
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"))) {
output += "\n anywhere\n </p>\n";
;
}
else {
output += "\n </p>\n <ul>\n ";
frame = frame.push();
var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets");
var t_7 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets");
if(t_7) {var t_6 = t_7.length;
for(var t_5=0; t_5 < t_7.length; t_5++) {
var t_8 = t_7[t_5];
@@ -545,7 +545,7 @@ output += "\n </ul>\n";
;
}
output += "\n\n<p>Autosign is allowed from:\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) {
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"autosign_subnets"))) {
output += "\n anywhere\n </p>\n";
;
}
@@ -575,14 +575,14 @@ output += "\n </ul>\n";
;
}
output += "\n\n<p>Authority administration is allowed from:\n";
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) {
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"))) {
output += "\n anywhere\n </p>\n";
;
}
else {
output += "\n <ul>\n ";
frame = frame.push();
var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets");
var t_15 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets");
if(t_15) {var t_14 = t_15.length;
for(var t_13=0; t_13 < t_15.length; t_13++) {
var t_16 = t_15[t_13];
@@ -606,15 +606,11 @@ output += "\n </ul>\n";
}
output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n";
frame = frame.push();
var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
if(t_19) {var t_17;
if(runtime.isArray(t_19)) {
var t_18 = t_19.length;
for(t_17=0; t_17 < t_19.length; t_17++) {
var t_20 = t_19[t_17][0]
frame.set("handle", t_19[t_17][0]);
var t_21 = t_19[t_17][1]
frame.set("full_name", t_19[t_17][1]);
var t_19 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_users");
if(t_19) {var t_18 = t_19.length;
for(var t_17=0; t_17 < t_19.length; t_17++) {
var t_20 = t_19[t_17];
frame.set("user", t_20);
frame.set("loop.index", t_17 + 1);
frame.set("loop.index0", t_17);
frame.set("loop.revindex", t_18 - t_17);
@@ -622,32 +618,15 @@ frame.set("loop.revindex0", t_18 - t_17 - 1);
frame.set("loop.first", t_17 === 0);
frame.set("loop.last", t_17 === t_18 - 1);
frame.set("loop.length", t_18);
output += "\n <li>";
output += runtime.suppressValue(t_21, env.opts.autoescape);
output += "</li>\n";
output += "\n <li><a href=\"mailto:";
output += runtime.suppressValue(runtime.memberLookup((t_20),"mail"), env.opts.autoescape);
output += "\">";
output += runtime.suppressValue(runtime.memberLookup((t_20),"given_name"), env.opts.autoescape);
output += " ";
output += runtime.suppressValue(runtime.memberLookup((t_20),"surname"), env.opts.autoescape);
output += "</a></li>\n";
;
}
} else {
t_17 = -1;
var t_18 = runtime.keys(t_19).length;
for(var t_22 in t_19) {
t_17++;
var t_23 = t_19[t_22];
frame.set("handle", t_22);
frame.set("full_name", t_23);
frame.set("loop.index", t_17 + 1);
frame.set("loop.index0", t_17);
frame.set("loop.revindex", t_18 - t_17);
frame.set("loop.revindex0", t_18 - t_17 - 1);
frame.set("loop.first", t_17 === 0);
frame.set("loop.last", t_17 === t_18 - 1);
frame.set("loop.length", t_18);
output += "\n <li>";
output += runtime.suppressValue(t_23, env.opts.autoescape);
output += "</li>\n";
;
}
}
}
frame = frame.pop();
output += "\n</ul>\n</section>\n\n";
@@ -658,14 +637,14 @@ output += "\n<p>Here you can renew your certificates</p>\n\n";
;
}
output += "\n\n";
var t_24;
t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
frame.set("s", t_24, true);
var t_21;
t_21 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
frame.set("s", t_21, true);
if(frame.topLevel) {
context.setVariable("s", t_24);
context.setVariable("s", t_21);
}
if(frame.topLevel) {
context.addExport("s", t_24);
context.addExport("s", t_21);
}
output += "\n\n\n";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
@@ -673,24 +652,24 @@ output += "\n<section id=\"requests\">\n <h1>Pending requests</h1>\n\n <p>
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
output += "</pre>\n\n <ul id=\"pending_requests\">\n ";
frame = frame.push();
var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_27) {var t_26 = t_27.length;
for(var t_25=0; t_25 < t_27.length; t_25++) {
var t_28 = t_27[t_25];
frame.set("request", t_28);
frame.set("loop.index", t_25 + 1);
frame.set("loop.index0", t_25);
frame.set("loop.revindex", t_26 - t_25);
frame.set("loop.revindex0", t_26 - t_25 - 1);
frame.set("loop.first", t_25 === 0);
frame.set("loop.last", t_25 === t_26 - 1);
frame.set("loop.length", t_26);
var t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_24) {var t_23 = t_24.length;
for(var t_22=0; t_22 < t_24.length; t_22++) {
var t_25 = t_24[t_22];
frame.set("request", t_25);
frame.set("loop.index", t_22 + 1);
frame.set("loop.index0", t_22);
frame.set("loop.revindex", t_23 - t_22);
frame.set("loop.revindex0", t_23 - t_22 - 1);
frame.set("loop.first", t_22 === 0);
frame.set("loop.last", t_22 === t_23 - 1);
frame.set("loop.length", t_23);
output += "\n ";
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) {
if(t_31) { cb(t_31); return; }
t_29.render(context.getVariables(), frame, function(t_32,t_30) {
if(t_32) { cb(t_32); return; }
output += t_30
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_28,t_26) {
if(t_28) { cb(t_28); return; }
t_26.render(context.getVariables(), frame, function(t_29,t_27) {
if(t_29) { cb(t_29); return; }
output += t_27
output += "\n\t ";
})});
}
@@ -698,24 +677,24 @@ output += "\n\t ";
frame = frame.pop();
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign!</p>\n </li>\n </ul>\n</section>\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\n ";
frame = frame.push();
var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
if(t_35) {var t_34 = t_35.length;
for(var t_33=0; t_33 < t_35.length; t_33++) {
var t_36 = t_35[t_33];
frame.set("certificate", t_36);
frame.set("loop.index", t_33 + 1);
frame.set("loop.index0", t_33);
frame.set("loop.revindex", t_34 - t_33);
frame.set("loop.revindex0", t_34 - t_33 - 1);
frame.set("loop.first", t_33 === 0);
frame.set("loop.last", t_33 === t_34 - 1);
frame.set("loop.length", t_34);
var t_32 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
if(t_32) {var t_31 = t_32.length;
for(var t_30=0; t_30 < t_32.length; t_30++) {
var t_33 = t_32[t_30];
frame.set("certificate", t_33);
frame.set("loop.index", t_30 + 1);
frame.set("loop.index0", t_30);
frame.set("loop.revindex", t_31 - t_30);
frame.set("loop.revindex0", t_31 - t_30 - 1);
frame.set("loop.first", t_30 === 0);
frame.set("loop.last", t_30 === t_31 - 1);
frame.set("loop.length", t_31);
output += "\n ";
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) {
if(t_39) { cb(t_39); return; }
t_37.render(context.getVariables(), frame, function(t_40,t_38) {
if(t_40) { cb(t_40); return; }
output += t_38
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_36,t_34) {
if(t_36) { cb(t_36); return; }
t_34.render(context.getVariables(), frame, function(t_37,t_35) {
if(t_37) { cb(t_37); return; }
output += t_35
output += "\n\t ";
})});
}
@@ -729,31 +708,31 @@ output += "/certificate/ > session.pem\n openssl ocsp -issuer session.pem -CA
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n ";
frame = frame.push();
var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
if(t_43) {var t_42 = t_43.length;
for(var t_41=0; t_41 < t_43.length; t_41++) {
var t_44 = t_43[t_41];
frame.set("j", t_44);
frame.set("loop.index", t_41 + 1);
frame.set("loop.index0", t_41);
frame.set("loop.revindex", t_42 - t_41);
frame.set("loop.revindex0", t_42 - t_41 - 1);
frame.set("loop.first", t_41 === 0);
frame.set("loop.last", t_41 === t_42 - 1);
frame.set("loop.length", t_42);
var t_40 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
if(t_40) {var t_39 = t_40.length;
for(var t_38=0; t_38 < t_40.length; t_38++) {
var t_41 = t_40[t_38];
frame.set("j", t_41);
frame.set("loop.index", t_38 + 1);
frame.set("loop.index0", t_38);
frame.set("loop.revindex", t_39 - t_38);
frame.set("loop.revindex0", t_39 - t_38 - 1);
frame.set("loop.first", t_38 === 0);
frame.set("loop.last", t_38 === t_39 - 1);
frame.set("loop.length", t_39);
output += "\n <li id=\"certificate_";
output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"sha256sum"), env.opts.autoescape);
output += "\">\n ";
output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"changed"), env.opts.autoescape);
output += "\n ";
output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"serial_number"), env.opts.autoescape);
output += " <span class=\"monospace\">";
output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"identity"), env.opts.autoescape);
output += "</span>\n </li>\n ";
;
}
}
if (!t_42) {
if (!t_39) {
output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
}
frame = frame.pop();
@@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
output += "</div>\n ";
})});
}
output += "\n \n ";
output += "\n\n ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
output += "\n <div class=\"person\">";
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {

View File

@@ -31,13 +31,13 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>Authenticated users allowed from:
{% if "0.0.0.0/0" in session.user_subnets %}
{% if "0.0.0.0/0" in session.authority.user_subnets %}
anywhere
</p>
{% else %}
</p>
<ul>
{% for i in session.user_subnets %}
{% for i in session.authority.user_subnets %}
<li>{{ i }}</li>
{% endfor %}
</ul>
@@ -46,20 +46,20 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>Request submission is allowed from:
{% if "0.0.0.0/0" in session.request_subnets %}
{% if "0.0.0.0/0" in session.authority.request_subnets %}
anywhere
</p>
{% else %}
</p>
<ul>
{% for subnet in session.request_subnets %}
{% for subnet in session.authority.request_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Autosign is allowed from:
{% if "0.0.0.0/0" in session.autosign_subnets %}
{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
anywhere
</p>
{% else %}
@@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b
{% endif %}
<p>Authority administration is allowed from:
{% if "0.0.0.0/0" in session.admin_subnets %}
{% if "0.0.0.0/0" in session.authority.admin_subnets %}
anywhere
</p>
{% else %}
<ul>
{% for subnet in session.admin_subnets %}
{% for subnet in session.authority.admin_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
@@ -86,8 +86,8 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>Authority administration allowed for:</p>
<ul>
{% for handle, full_name in session.admin_users %}
<li>{{ full_name }}</li>
{% for user in session.authority.admin_users %}
<li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li>
{% endfor %}
</ul>
</section>

View File

@@ -57,4 +57,4 @@ certificate path = {{ ca_crt }}
requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/
outbox = smtp://localhost
outbox = {{ outbox }}

View File

@@ -0,0 +1,6 @@
Certificate {{certificate.common_name}} ({{certificate.serial_number}}) revoked
This is simply to notify that certificate {{ certificate.common_name }}
was revoked.
Services making use of this certificates might become unavailable.

View File

@@ -0,0 +1,5 @@
Certificate signing request {{request.common_name}} stored
This is simply to notify that certificate signing request for {{ request.common_name }}
was stored. You may log in with a certificate authority administration account to sign it.

View File

@@ -15,6 +15,7 @@ server {
ssl_certificate {{certificate_path}};
ssl_certificate_key {{key_path}};
ssl_client_certificate {{authority_path}};
ssl_crl {{revocations_path}};
ssl_verify_client {{verify_client}};
}

View File

@@ -7,9 +7,6 @@ events {
}
http {
{% if not push_server %}
push_stream_shared_memory_size 32M;
{% endif %}
include mime.types;
default_type application/octet-stream;
sendfile on;
@@ -21,7 +18,7 @@ http {
}
server {
server_name {{hostname}};
server_name {{hostname}}; # TODO: FQDN, SSL
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
error_page 500 502 503 504 /50x.html;

View File

@@ -2,7 +2,7 @@ client
remote {{remote}}
remote-cert-tls server
proto {{proto}}
dev tap0
dev tap
nobind
key {{key_path}}
cert {{certificate_path}}
@@ -10,4 +10,5 @@ ca {{authority_path}}
comp-lzo
user nobody
group nogroup
persist-tun
persist-key

View File

@@ -2,15 +2,18 @@ mode server
tls-server
proto {{proto}}
port {{port}}
dev tap0
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}}

View File

@@ -5,7 +5,7 @@ processes = 1
vacuum = true
uid = {{username}}
gid = {{username}}
plugins = python34
plugins = python27
chdir = /tmp
module = certidude.wsgi
callable = app

165
certidude/user.py Normal file
View File

@@ -0,0 +1,165 @@
import click
import grp
import ldap
import ldap.sasl
import os
import pwd
from certidude import constants, config
class User(object):
def __init__(self, username, mail, given_name="", surname=""):
if "@" not in mail:
raise ValueError("Invalid e-mail %s" % repr(mail))
self.name = username
self.mail = mail
self.given_name = given_name
self.surname = surname
def __unicode__(self):
if self.given_name and self.surname:
return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
else:
return self.mail
def __hash__(self):
return hash(self.mail)
def __eq__(self, other):
return self.mail == other.mail
def __repr__(self):
return unicode(self).encode("utf-8")
def is_admin(self):
if not hasattr(self, "_is_admin"):
self._is_admin = self.objects.is_admin(self)
return self._is_admin
class DoesNotExist(StandardError):
pass
class PosixUserManager(object):
def get(self, username):
_, _, _, _, gecos, _, _ = pwd.getpwnam(username)
gecos = gecos.decode("utf-8").split(",")
full_name = gecos[0]
mail = username + "@" + constants.DOMAIN
if full_name and " " in full_name:
given_name, surname = full_name.split(" ", 1)
return User(username, mail, given_name, surname)
return User(username, mail)
def filter_admins(self):
_, _, gid, members = grp.getgrnam(config.ADMIN_GROUP)
for username in members:
yield self.get(username)
def is_admin(self, username):
import grp
_, _, gid, members = grp.getgrnam(config.ADMIN_GROUP)
return username in members
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!")
os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE
for server in config.LDAP_SERVERS:
self.conn = ldap.initialize(server)
self.conn.set_option(ldap.OPT_REFERRALS, 0)
click.echo("Connecing to %s using Kerberos ticket cache from %s" %
(server, config.LDAP_GSSAPI_CRED_CACHE))
self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
return self.conn
raise ValueError("No LDAP servers specified!")
def __exit__(self, type, value, traceback):
self.conn.unbind_s
class ActiveDirectoryUserManager(object):
def get(self, username):
# TODO: Sanitize username
if "@" in username:
username, _ = username.split("@", 1)
with DirectoryConnection() as conn:
ft = config.LDAP_USER_FILTER % username
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), attribs)
for dn, entry in r:
if not dn:
continue
if entry.get("givenname") and entry.get("sn"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")
else:
cn, = entry.get("cn")
if " " in cn:
given_name, surname = cn.split(" ", 1)
else:
given_name, surname = cn, ""
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.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)
def filter(self, ft):
with DirectoryConnection() as conn:
attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), attribs)
for dn,entry in r:
if not dn:
continue
username, = entry.get("sAMAccountName")
cn, = entry.get("cn")
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,)
if entry.get("givenName") and entry.get("sn"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")
else:
cn, = entry.get("cn")
if " " in cn:
given_name, surname = cn.split(" ", 1)
else:
given_name, surname = cn, ""
yield User(username.decode("utf-8"), mail.decode("utf-8"),
given_name.decode("utf-8"), surname.decode("utf-8"))
def filter_admins(self):
"""
Return admin User objects
"""
return self.filter(config.LDAP_ADMIN_FILTER % "*")
def all(self):
"""
Return all valid User objects
"""
return self.filter(ft=config.LDAP_USER_FILTER % "*")
def is_admin(self, user):
with DirectoryConnection() as conn:
ft = config.LDAP_ADMIN_FILTER % user.name
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), ["cn"])
for dn, entry in r:
if not dn:
continue
return True
return False
if config.ACCOUNTS_BACKEND == "ldap":
User.objects = ActiveDirectoryUserManager()
elif config.ACCOUNTS_BACKEND == "posix":
User.objects = PosixUserManager()
else:
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)