1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Several updates #2

* Reverse RDN components for all certs
* Less side effects in unittests
* Split help dialog shell snippets into separate files
* Restore 'admin subnets' config option
* Embedded subnets, IKE and ESP proposals now configurable in builder.conf
* Use expr instead of bc for math operations in shell
* Better frontend support for Let's Encrypt certificates
This commit is contained in:
Lauri Võsandi 2018-05-02 08:11:01 +00:00
parent 5e9251f365
commit 4e4b551cc2
49 changed files with 959 additions and 1051 deletions

View File

@ -7,12 +7,12 @@ import os
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from xattr import listxattr, getxattr from xattr import listxattr, getxattr
from certidude.auth import login_required
from certidude.common import cert_to_dn from certidude.common import cert_to_dn
from certidude.user import User from certidude.user import User
from certidude.decorators import serialize, csrf_protection from certidude.decorators import serialize, csrf_protection
from certidude import const, config, authority from certidude import const, config, authority
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,6 +30,7 @@ class SessionResource(AuthorityHandler):
@csrf_protection @csrf_protection
@serialize @serialize
@login_required @login_required
@authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
def serialize_requests(g): def serialize_requests(g):
@ -43,7 +44,6 @@ class SessionResource(AuthorityHandler):
except IOError: except IOError:
submission_hostname = None submission_hostname = None
yield dict( yield dict(
server = self.authority.server_flags(common_name),
submitted = submitted, submitted = submitted,
common_name = common_name, common_name = common_name,
address = submission_address, address = submission_address,
@ -55,7 +55,7 @@ class SessionResource(AuthorityHandler):
) )
def serialize_revoked(g): def serialize_revoked(g):
for common_name, path, buf, cert, signed, expired, revoked, reason in g(): for common_name, path, buf, cert, signed, expired, revoked, reason in g(limit=5):
yield dict( yield dict(
serial = "%x" % cert.serial_number, serial = "%x" % cert.serial_number,
common_name = common_name, common_name = common_name,
@ -121,10 +121,7 @@ class SessionResource(AuthorityHandler):
if e["extn_id"].native in ("extended_key_usage",)]) if e["extn_id"].native in ("extended_key_usage",)])
) )
if req.context.get("user").is_admin(): logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
else:
logger.info("Logged in authority user %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
return dict( return dict(
user = dict( user = dict(
name=req.context.get("user").name, name=req.context.get("user").name,
@ -175,14 +172,15 @@ class SessionResource(AuthorityHandler):
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
) )
) if req.context.get("user").is_admin() else None, ),
features=dict( features=dict(
ocsp=bool(config.OCSP_SUBNETS), ocsp=bool(config.OCSP_SUBNETS),
crl=bool(config.CRL_SUBNETS), crl=bool(config.CRL_SUBNETS),
token=bool(config.TOKEN_URL), token=bool(config.TOKEN_URL),
tagging=True, tagging=True,
leases=True, leases=True,
logging=config.LOGGING_BACKEND)) logging=config.LOGGING_BACKEND)
)
class StaticResource(object): class StaticResource(object):

View File

@ -4,9 +4,7 @@ import re
from xattr import setxattr, listxattr, removexattr from xattr import setxattr, listxattr, removexattr
from certidude import push from certidude import push
from certidude.decorators import serialize, csrf_protection from certidude.decorators import serialize, csrf_protection
from certidude.auth import login_required, authorize_admin from .utils.firewall import login_required, authorize_admin, whitelist_subject
from .utils.firewall import whitelist_subject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -5,9 +5,10 @@ import logging
import os import os
import subprocess import subprocess
from certidude import config, const, authority from certidude import config, const, authority
from certidude.auth import login_required, authorize_admin
from certidude.common import cert_to_dn from certidude.common import cert_to_dn
from ipaddress import ip_network
from jinja2 import Template from jinja2 import Template
from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,6 +18,7 @@ class ImageBuilderResource(object):
def on_get(self, req, resp, profile, suggested_filename): def on_get(self, req, resp, profile, suggested_filename):
router = [j[0] for j in authority.list_signed( router = [j[0] for j in authority.list_signed(
common_name=config.cp2.get(profile, "router"))][0] common_name=config.cp2.get(profile, "router"))][0]
subnets = set([ip_network(j) for j in config.cp2.get(profile, "subnets").split(" ")])
model = config.cp2.get(profile, "model") model = config.cp2.get(profile, "model")
build_script_path = config.cp2.get(profile, "command") build_script_path = config.cp2.get(profile, "command")
overlay_path = config.cp2.get(profile, "overlay") overlay_path = config.cp2.get(profile, "overlay")
@ -40,6 +42,9 @@ class ImageBuilderResource(object):
cwd=os.path.dirname(os.path.realpath(build_script_path)), cwd=os.path.dirname(os.path.realpath(build_script_path)),
env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin", env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
"ROUTER": router, "ROUTER": router,
"IKE": config.cp2.get(profile, "ike"),
"ESP": config.cp2.get(profile, "esp"),
"SUBNETS": ",".join(str(j) for j in subnets),
"AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm, "AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm,
"AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate), "AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate),
"BUILD":build, "OVERLAY":build + "/overlay/"}, "BUILD":build, "OVERLAY":build + "/overlay/"},

View File

@ -5,9 +5,9 @@ import re
import xattr import xattr
from datetime import datetime from datetime import datetime
from certidude import config, push from certidude import config, push
from certidude.auth import login_required, authorize_admin, authorize_server
from certidude.decorators import serialize from certidude.decorators import serialize
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin, authorize_server
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,7 +1,7 @@
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.relational import RelationalMixin from certidude.relational import RelationalMixin
from .utils.firewall import login_required, authorize_admin
class LogResource(RelationalMixin): class LogResource(RelationalMixin):
SQL_CREATE_TABLES = "log_tables.sql" SQL_CREATE_TABLES = "log_tables.sql"

View File

@ -8,7 +8,6 @@ from asn1crypto import pem, x509
from asn1crypto.csr import CertificationRequest from asn1crypto.csr import CertificationRequest
from base64 import b64decode from base64 import b64decode
from certidude import config, push, errors from certidude import config, push, errors
from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import csrf_protection, MyEncoder from certidude.decorators import csrf_protection, MyEncoder
from certidude.profile import SignatureProfile from certidude.profile import SignatureProfile
from datetime import datetime from datetime import datetime
@ -16,7 +15,8 @@ from oscrypto import asymmetric
from oscrypto.errors import SignatureError from oscrypto.errors import SignatureError
from xattr import getxattr, setxattr from xattr import getxattr, setxattr
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets, whitelist_content_types from .utils.firewall import whitelist_subnets, whitelist_content_types, \
login_required, login_optional, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -219,7 +219,6 @@ class RequestDetailResource(AuthorityHandler):
resp.body = json.dumps(dict( resp.body = json.dumps(dict(
submitted = submitted, submitted = submitted,
common_name = cn, common_name = cn,
server = self.authority.server_flags(cn),
address = getxattr(path, "user.request.address").decode("ascii"), # TODO: move to authority.py address = getxattr(path, "user.request.address").decode("ascii"), # TODO: move to authority.py
md5sum = hashlib.md5(buf).hexdigest(), md5sum = hashlib.md5(buf).hexdigest(),
sha1sum = hashlib.sha1(buf).hexdigest(), sha1sum = hashlib.sha1(buf).hexdigest(),

View File

@ -3,10 +3,10 @@ import falcon
import logging import logging
import json import json
import hashlib import hashlib
from certidude.auth import login_required, authorize_admin
from certidude.decorators import csrf_protection from certidude.decorators import csrf_protection
from xattr import listxattr, getxattr from xattr import listxattr, getxattr
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,9 +1,9 @@
import logging import logging
from xattr import getxattr, removexattr, setxattr from xattr import getxattr, removexattr, setxattr
from certidude import push from certidude import push
from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize, csrf_protection from certidude.decorators import serialize, csrf_protection
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -9,8 +9,8 @@ from certidude import mailer
from certidude.decorators import serialize from certidude.decorators import serialize
from certidude.user import User from certidude.user import User
from certidude import config from certidude import config
from certidude.auth import login_required, authorize_admin
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,7 +1,16 @@
import falcon import falcon
import logging import logging
import binascii
import click
import gssapi
import os
import re
import socket
from asn1crypto import pem, x509 from asn1crypto import pem, x509
from base64 import b64decode
from certidude.user import User
from certidude import config, const
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -68,3 +77,200 @@ def whitelist_subject(func):
else: else:
return func(self, req, resp, cn, *args, **kwargs) return func(self, req, resp, cn, *args, **kwargs)
return wrapped return wrapped
def authenticate(optional=False):
def wrapper(func):
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
# Try pre-emptive authentication
if not req.auth:
if optional:
req.context["user"] = None
return func(resource, req, resp, *args, **kwargs)
logger.debug("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?",
["Negotiate"])
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
try:
server_creds = gssapi.creds.Credentials(
usage='accept',
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
except gssapi.raw.exceptions.BadNameError:
logger.error("Failed initialize HTTP service principal, possibly bad permissions for %s or /etc/krb5.conf" %
config.KERBEROS_KEYTAB)
raise
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
if not req.auth.startswith("Negotiate "):
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Negotiate: %s" % req.auth)
token = ''.join(req.auth.split()[1:])
try:
context.step(b64decode(token))
except binascii.Error: # base64 errors
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
except gssapi.raw.exceptions.BadMechanismError:
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
try:
username, realm = str(context.initiator_name).split("@")
except AttributeError: # TODO: Better exception
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
if realm != config.KERBEROS_REALM:
raise falcon.HTTPForbidden("Forbidden",
"Cross-realm trust not supported")
if username.endswith("$") and optional:
# Extract machine hostname
# TODO: Assert LDAP group membership
req.context["machine"] = username[:-1].lower()
req.context["user"] = None
else:
# Attempt to look up real user
req.context["user"] = User.objects.get(username)
logger.debug("Succesfully authenticated user %s for %s from %s",
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
return func(resource, req, resp, *args, **kwargs)
def ldap_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against LDAP with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
import ldap
if not req.auth:
raise falcon.HTTPUnauthorized("Unauthorized",
"No authentication header provided",
("Basic",))
if not req.auth.startswith("Basic "):
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
from base64 import b64decode
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1)
upn = "%s@%s" % (user, const.DOMAIN)
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn))
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
conn.set_option(ldap.OPT_REFERRALS, 0)
try:
conn.simple_bind_s(upn, passwd)
except ldap.STRONG_AUTH_REQUIRED:
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
raise
except ldap.SERVER_DOWN:
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
config.LDAP_AUTHENTICATION_URI)
raise
except ldap.INVALID_CREDENTIALS:
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 username" % const.DOMAIN,
("Basic",))
req.context["ldap_conn"] = conn
req.context["user"] = User.objects.get(user)
retval = func(resource, req, resp, *args, **kwargs)
conn.unbind_s()
return retval
def pam_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against PAM with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
if not req.auth:
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",))
if not req.auth.startswith("Basic "):
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1)
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
logger.critical("Basic authentication failed for user %s from %s, "
"are you sure server process has read access to /etc/shadow?",
repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",))
req.context["user"] = User.objects.get(user)
return func(resource, req, resp, *args, **kwargs)
def wrapped(resource, req, resp, *args, **kwargs):
# If LDAP enabled and device is not Kerberos capable fall
# back to LDAP bind authentication
if "ldap" in config.AUTHENTICATION_BACKENDS:
if "Android" in req.user_agent or "iPhone" in req.user_agent:
return ldap_authenticate(resource, req, resp, *args, **kwargs)
if "kerberos" in config.AUTHENTICATION_BACKENDS:
return kerberos_authenticate(resource, req, resp, *args, **kwargs)
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
return pam_authenticate(resource, req, resp, *args, **kwargs)
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
return ldap_authenticate(resource, req, resp, *args, **kwargs)
else:
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
return wrapped
return wrapper
def login_required(func):
return authenticate()(func)
def login_optional(func):
return authenticate(optional=True)(func)
def authorize_admin(func):
@whitelist_subnets(config.ADMIN_SUBNETS)
def wrapped(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")
return wrapped
def authorize_server(func):
"""
Make sure the request originator has a certificate with server flags
"""
from asn1crypto import pem, x509
def wrapped(resource, req, resp, *args, **kwargs):
buf = req.get_header("X-SSL-CERT")
if not buf:
logger.info("No TLS certificate presented to access administrative API call")
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
cert = x509.Certificate.load(der_bytes) # TODO: validate serial
for extension in cert["tbs_certificate"]["extensions"]:
if extension["extn_id"].native == "extended_key_usage":
if "server_auth" in extension["extn_value"].native:
req.context["machine"] = cert.subject.native["common_name"]
return func(resource, req, resp, *args, **kwargs)
logger.info("TLS authenticated machine '%s' not authorized to access administrative API", cert.subject.native["common_name"])
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
return wrapped

View File

@ -1,209 +0,0 @@
import binascii
import click
import gssapi
import falcon
import logging
import os
import re
import socket
from base64 import b64decode
from certidude.user import User
from certidude import config, const
logger = logging.getLogger("api")
def authenticate(optional=False):
def wrapper(func):
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
# Try pre-emptive authentication
if not req.auth:
if optional:
req.context["user"] = None
return func(resource, req, resp, *args, **kwargs)
logger.debug("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?",
["Negotiate"])
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
try:
server_creds = gssapi.creds.Credentials(
usage='accept',
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
except gssapi.raw.exceptions.BadNameError:
logger.error("Failed initialize HTTP service principal, possibly bad permissions for %s or /etc/krb5.conf" %
config.KERBEROS_KEYTAB)
raise
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
if not req.auth.startswith("Negotiate "):
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
token = ''.join(req.auth.split()[1:])
try:
context.step(b64decode(token))
except binascii.Error: # base64 errors
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
except gssapi.raw.exceptions.BadMechanismError:
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
try:
username, realm = str(context.initiator_name).split("@")
except AttributeError: # TODO: Better exception
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
if realm != config.KERBEROS_REALM:
raise falcon.HTTPForbidden("Forbidden",
"Cross-realm trust not supported")
if username.endswith("$") and optional:
# Extract machine hostname
# TODO: Assert LDAP group membership
req.context["machine"] = username[:-1].lower()
req.context["user"] = None
else:
# Attempt to look up real user
req.context["user"] = User.objects.get(username)
logger.debug("Succesfully authenticated user %s for %s from %s",
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
return func(resource, req, resp, *args, **kwargs)
def ldap_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against LDAP with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
import ldap
if not req.auth:
raise falcon.HTTPUnauthorized("Unauthorized",
"No authentication header provided",
("Basic",))
if not req.auth.startswith("Basic "):
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
from base64 import b64decode
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1)
upn = "%s@%s" % (user, const.DOMAIN)
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn))
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
conn.set_option(ldap.OPT_REFERRALS, 0)
try:
conn.simple_bind_s(upn, passwd)
except ldap.STRONG_AUTH_REQUIRED:
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
raise
except ldap.SERVER_DOWN:
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
config.LDAP_AUTHENTICATION_URI)
raise
except ldap.INVALID_CREDENTIALS:
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 username" % const.DOMAIN,
("Basic",))
req.context["ldap_conn"] = conn
req.context["user"] = User.objects.get(user)
retval = func(resource, req, resp, *args, **kwargs)
conn.unbind_s()
return retval
def pam_authenticate(resource, req, resp, *args, **kwargs):
"""
Authenticate against PAM with WWW Basic Auth credentials
"""
if optional and not req.get_param_as_bool("authenticate"):
return func(resource, req, resp, *args, **kwargs)
if not req.auth:
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",))
if not req.auth.startswith("Basic "):
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1)
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
logger.critical("Basic authentication failed for user %s from %s, "
"are you sure server process has read access to /etc/shadow?",
repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",))
req.context["user"] = User.objects.get(user)
return func(resource, req, resp, *args, **kwargs)
def wrapped(resource, req, resp, *args, **kwargs):
# If LDAP enabled and device is not Kerberos capable fall
# back to LDAP bind authentication
if "ldap" in config.AUTHENTICATION_BACKENDS:
if "Android" in req.user_agent or "iPhone" in req.user_agent:
return ldap_authenticate(resource, req, resp, *args, **kwargs)
if "kerberos" in config.AUTHENTICATION_BACKENDS:
return kerberos_authenticate(resource, req, resp, *args, **kwargs)
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
return pam_authenticate(resource, req, resp, *args, **kwargs)
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
return ldap_authenticate(resource, req, resp, *args, **kwargs)
else:
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
return wrapped
return wrapper
def login_required(func):
return authenticate()(func)
def login_optional(func):
return authenticate(optional=True)(func)
def authorize_admin(func):
def wrapped(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")
return wrapped
def authorize_server(func):
"""
Make sure the request originator has a certificate with server flags
"""
from asn1crypto import pem, x509
def wrapped(resource, req, resp, *args, **kwargs):
buf = req.get_header("X-SSL-CERT")
if not buf:
logger.info("No TLS certificate presented to access administrative API call")
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
cert = x509.Certificate.load(der_bytes) # TODO: validate serial
for extension in cert["tbs_certificate"]["extensions"]:
if extension["extn_id"].native == "extended_key_usage":
if "server_auth" in extension["extn_value"].native:
req.context["machine"] = cert.subject.native["common_name"]
return func(resource, req, resp, *args, **kwargs)
logger.info("TLS authenticated machine '%s' not authorized to access administrative API", cert.subject.native["common_name"])
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
return wrapped

View File

@ -48,7 +48,7 @@ with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
header, _, key_der_bytes = pem.unarmor(key_buf) header, _, key_der_bytes = pem.unarmor(key_buf)
private_key = asymmetric.load_private_key(key_der_bytes) private_key = asymmetric.load_private_key(key_der_bytes)
def self_enroll(): def self_enroll(skip_notify=False):
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root" assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
from certidude import const from certidude import const
@ -87,7 +87,7 @@ def self_enroll():
click.echo("Writing request to %s" % path) click.echo("Writing request to %s" % path)
with open(path, "wb") as fh: with open(path, "wb") as fh:
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) authority.sign(common_name, skip_notify=skip_notify, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
sys.exit(0) sys.exit(0)
else: else:
os.waitpid(pid, 0) os.waitpid(pid, 0)
@ -243,22 +243,10 @@ def revoke(common_name, reason):
attach_cert = buf, "application/x-pem-file", common_name + ".crt" attach_cert = buf, "application/x-pem-file", common_name + ".crt"
mailer.send("certificate-revoked.md", mailer.send("certificate-revoked.md",
attachments=(attach_cert,), attachments=(attach_cert,),
serial_hex="%040x" % cert.serial_number, serial_hex="%x" % cert.serial_number,
common_name=common_name) common_name=common_name)
return revoked_path return revoked_path
def server_flags(cn):
if config.USER_ENROLLMENT_ALLOWED and not config.USER_MULTIPLE_CERTIFICATES:
# Common name set to username, used for only HTTPS client validation anyway
return False
if "@" in cn:
# username@hostname is user certificate anyway, can't be server
return False
if "." in cn:
# CN is hostname, if contains dot has to be FQDN, hence a server
return True
return False
def list_requests(directory=config.REQUESTS_DIR): def list_requests(directory=config.REQUESTS_DIR):
for filename in os.listdir(directory): for filename in os.listdir(directory):
@ -297,12 +285,16 @@ def list_signed(directory=config.SIGNED_DIR, common_name=None):
path, buf, cert, signed, expires = get_signed(basename) path, buf, cert, signed, expires = get_signed(basename)
yield basename, path, buf, cert, signed, expires yield basename, path, buf, cert, signed, expires
def list_revoked(directory=config.REVOKED_DIR): def list_revoked(directory=config.REVOKED_DIR, limit=0):
for filename in os.listdir(directory): for filename in sorted(os.listdir(directory), reverse=True):
if filename.endswith(".pem"): if filename.endswith(".pem"):
common_name = filename[:-4] common_name = filename[:-4]
path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name) path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name)
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason
if limit:
limit -= 1
if limit <= 0:
return
def list_server_names(): def list_server_names():
@ -324,7 +316,10 @@ def export_crl(pem=True):
serial_number = filename[:-4] serial_number = filename[:-4]
# TODO: Assert serial against regex # TODO: Assert serial against regex
revoked_path = os.path.join(config.REVOKED_DIR, filename) revoked_path = os.path.join(config.REVOKED_DIR, filename)
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup try:
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
except IOError: # TODO: make sure it's not required
reason = "key_compromise"
# TODO: Skip expired certificates # TODO: Skip expired certificates
s = os.stat(revoked_path) s = os.stat(revoked_path)
@ -404,7 +399,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
if overwrite: if overwrite:
# TODO: is this the best approach? # TODO: is this the best approach?
prev_serial_hex = "%040x" % prev.serial_number prev_serial_hex = "%x" % prev.serial_number
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex) revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
os.rename(cert_path, revoked_path) os.rename(cert_path, revoked_path)
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
@ -433,7 +428,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
os.rename(cert_path + ".part", cert_path) os.rename(cert_path + ".part", cert_path)
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
cert_serial_hex = "%040x" % end_entity_cert.serial_number cert_serial_hex = "%x" % end_entity_cert.serial_number
# Create symlink # Create symlink
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number) link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number)

View File

@ -24,7 +24,6 @@ from glob import glob
from ipaddress import ip_network from ipaddress import ip_network
from oscrypto import asymmetric from oscrypto import asymmetric
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml # http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
@ -569,6 +568,10 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
nm_config.set("vpn", "key", key_path) nm_config.set("vpn", "key", key_path)
nm_config.set("vpn", "cert", certificate_path) nm_config.set("vpn", "cert", certificate_path)
nm_config.set("vpn", "ca", authority_path) nm_config.set("vpn", "ca", authority_path)
nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-128-GCM-SHA384" % (
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
nm_config.set("vpn", "cipher", "AES-128-GCM")
nm_config.set("vpn", "auth", "SHA384")
nm_config.add_section("ipv4") nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto") nm_config.set("ipv4", "method", "auto")
nm_config.set("ipv4", "never-default", "true") nm_config.set("ipv4", "never-default", "true")
@ -617,6 +620,11 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
nm_config.set("vpn", "userkey", key_path) nm_config.set("vpn", "userkey", key_path)
nm_config.set("vpn", "usercert", certificate_path) nm_config.set("vpn", "usercert", certificate_path)
nm_config.set("vpn", "certificate", authority_path) nm_config.set("vpn", "certificate", authority_path)
dhgroup = "ecp384" if authority_public_key.algorithm == "ec" else "modp2048"
nm_config.set("vpn", "ike", "aes256-sha384-prfsha384-" + dhgroup)
nm_config.set("vpn", "esp", "aes128gcm16-aes128gmac-" + dhgroup)
nm_config.set("vpn", "proposal", "yes")
nm_config.add_section("ipv4") nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto") nm_config.set("ipv4", "method", "auto")
@ -982,13 +990,12 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--organizational-unit", "-ou", default="Certificate Authority") @click.option("--organizational-unit", "-ou", default="Certificate Authority")
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN) @click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", help="Directory for authority files") @click.option("--directory", help="Directory for authority files")
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN) @click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages") @click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@fqdn_required @fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve): def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_packages, elliptic_curve):
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) == b"xenial\n", "Only Ubuntu 16.04 supported at the moment" assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
import pwd import pwd
@ -1047,6 +1054,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
ca_cert = os.path.join(directory, "ca_cert.pem") ca_cert = os.path.join(directory, "ca_cert.pem")
sqlite_path = os.path.join(directory, "meta", "db.sqlite") sqlite_path = os.path.join(directory, "meta", "db.sqlite")
# Builder variables
dhgroup = "ecp384" if elliptic_curve else "modp2048"
try: try:
pwd.getpwnam("certidude") pwd.getpwnam("certidude")
click.echo("User 'certidude' already exists") click.echo("User 'certidude' already exists")
@ -1084,6 +1094,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
else: else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
letsencrypt = os.path.exists(letsencrypt_fullchain)
doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc") doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script") script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")
@ -1140,9 +1153,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg") os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg")
# Compile nunjucks templates # Compile nunjucks templates
cmd = 'nunjucks-precompile --include ".html$" --include ".svg" %s > %s.part' % (static_path, bundle_js) cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
click.echo("Compiling templates: %s" % cmd) click.echo("Compiling templates: %s" % cmd)
os.system(cmd) assert os.system(cmd) == 0
# Assemble bundle.js # Assemble bundle.js
click.echo("Assembling %s" % bundle_js) click.echo("Assembling %s" % bundle_js)
@ -1265,8 +1278,17 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
else: else:
os.waitpid(bootstrap_pid, 0) os.waitpid(bootstrap_pid, 0)
from certidude import authority from certidude import authority
authority.self_enroll() authority.self_enroll(skip_notify=True)
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
click.echo("Enabling and starting Certidude backend")
os.system("systemctl enable certidude")
os.system("systemctl restart certidude")
click.echo("Enabling and starting nginx")
os.system("systemctl enable nginx")
os.system("systemctl start nginx")
os.system("systemctl reload nginx")
click.echo()
click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH) click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo() click.echo()
click.echo("Use following commands to inspect the newly created files:") click.echo("Use following commands to inspect the newly created files:")
@ -1274,11 +1296,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert) click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert)
click.echo(" openssl rsa -check -in %s" % ca_key) click.echo(" openssl rsa -check -in %s" % ca_key)
click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert)) click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
click.echo()
click.echo("To enable and start the service:")
click.echo()
click.echo(" systemctl enable certidude")
click.echo(" systemctl start certidude")
return 0 return 0

View File

@ -23,22 +23,12 @@ def cn_to_dn(common_name, namespace, o=None, ou=None):
from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName
rdns = [] rdns = []
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("common_name"),
'value': DirectoryString(
name="utf8_string",
value=UTF8String(common_name))
})
]))
if ou: for dc in reversed(namespace.split(".")):
rdns.append(RelativeDistinguishedName([ rdns.append(RelativeDistinguishedName([
NameTypeAndValue({ NameTypeAndValue({
'type': NameType.map("organizational_unit_name"), 'type': NameType.map("domain_component"),
'value': DirectoryString( 'value': DNSName(value=dc)
name="utf8_string",
value=UTF8String(ou))
}) })
])) ]))
@ -52,14 +42,25 @@ def cn_to_dn(common_name, namespace, o=None, ou=None):
}) })
])) ]))
for dc in namespace.split("."): if ou:
rdns.append(RelativeDistinguishedName([ rdns.append(RelativeDistinguishedName([
NameTypeAndValue({ NameTypeAndValue({
'type': NameType.map("domain_component"), 'type': NameType.map("organizational_unit_name"),
'value': DNSName(value=dc) 'value': DirectoryString(
name="utf8_string",
value=UTF8String(ou))
}) })
])) ]))
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("common_name"),
'value': DirectoryString(
name="utf8_string",
value=UTF8String(common_name))
})
]))
return Name(name='', value=RDNSequence(rdns)) return Name(name='', value=RDNSequence(rdns))
def selinux_fixup(path): def selinux_fixup(path):

View File

@ -27,7 +27,7 @@ LDAP_BASE = cp.get("accounts", "ldap base")
USER_SUBNETS = set([ipaddress.ip_network(j) for j in USER_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "user subnets").split(" ") if j]) cp.get("authorization", "user subnets").split(" ") if j])
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS) cp.get("authorization", "admin subnets").split(" ") if j])
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "autosign subnets").split(" ") if j]) cp.get("authorization", "autosign subnets").split(" ") if j])
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in

View File

@ -42,7 +42,7 @@ def csrf_protection(func):
class MyEncoder(json.JSONEncoder): class MyEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
from certidude.auth import User from certidude.user import User
if isinstance(obj, ipaddress._IPAddressBase): if isinstance(obj, ipaddress._IPAddressBase):
return str(obj) return str(obj)
if isinstance(obj, set): if isinstance(obj, set):

View File

@ -19,40 +19,23 @@
<div class="collapse navbar-collapse" id="navbarsExampleDefault"> <div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Dashboard <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#">Settings</a> <a class="nav-link disabled dashboard" href="#">Dashboard</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link disabled log" href="#">Log</a> <a class="nav-link disabled log" href="#">Log</a>
</li> </li>
</ul> </ul>
<form class="form-inline my-2 my-lg-0"> <form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="search" placeholder="🔍"> <input id="search" class="form-control mr-sm-2" type="search" placeholder="🔍">
</form> </form>
</div> </div>
</nav> </nav>
<div id="view-dashboard" class="container-fluid"> <div id="view-dashboard" class="container-fluid" style="margin: 5em 0 0 0;">
<div id="view"> <div class="loader-container">
<div class="loader-container"> <div class="loader"></div>
<div class="loader"></div> <p>Loading certificate authority...</p>
<p>Loading certificate authority...</p>
</div>
</div>
</div>
<div id="view-log" class="container-fluid" style="margin: 5em 0 0 0; display: none;">
<h1>Log</h1>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked> Critical</label>
<label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label>
<label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label>
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</label>
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off"> Debug</label>
</div> </div>
<ul id="log-entries" class="list-group">
</ul>
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">

View File

@ -25,9 +25,24 @@ function onHashChanged() {
query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || '');
} }
if (query.columns) { query.columns = parseInt(query.columns) };
if (query.columns < 2 || query.columns > 4) {
query.columns = 2;
}
console.info("Hash is now:", query); console.info("Hash is now:", query);
loadAuthority(); if (window.location.protocol != "https:") {
$.get("/api/certificate/", function(blob) {
$("#view-dashboard").html(env.render('views/insecure.html', { window: window,
authority_name: window.location.hostname,
session: { authority: { certificate: { blob: blob }}}
}));
});
} else {
loadAuthority(query);
}
} }
function onTagClicked(tag) { function onTagClicked(tag) {
@ -277,12 +292,9 @@ function onSendToken() {
alert(e); alert(e);
} }
}); });
} }
function loadAuthority() { function loadAuthority(query) {
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
$.ajax({ $.ajax({
method: "GET", method: "GET",
@ -294,7 +306,7 @@ function loadAuthority() {
} else { } else {
var msg = { title: "Error " + response.status, description: response.statusText } var msg = { title: "Error " + response.status, description: response.statusText }
} }
$("#view").html(env.render('views/error.html', { message: msg })); $("#view-dashboard").html(env.render('views/error.html', { message: msg }));
}, },
success: function(session, status, xhr) { success: function(session, status, xhr) {
window.session = session; window.session = session;
@ -302,10 +314,18 @@ function loadAuthority() {
console.info("Loaded:", session); console.info("Loaded:", session);
$("#login").hide(); $("#login").hide();
if (!query.columns) {
query.columns = 2;
}
/** /**
* Render authority views * Render authority views
**/ **/
$("#view").html(env.render('views/authority.html', { session: session, window: window, $("#view-dashboard").html(env.render('views/authority.html', {
session: session,
window: window,
columns: query.columns,
column_width: 12 / query.columns,
authority_name: window.location.hostname })); authority_name: window.location.hostname }));
$("time").timeago(); $("time").timeago();
if (session.authority) { if (session.authority) {
@ -320,7 +340,7 @@ function loadAuthority() {
console.info("Opening EventSource from:", session.authority.events); console.info("Opening EventSource from:", session.authority.events);
var source = new EventSource(session.authority.events); window.source = new EventSource(session.authority.events);
source.onmessage = function(event) { source.onmessage = function(event) {
console.log("Received server-sent event:", event); console.log("Received server-sent event:", event);
@ -337,15 +357,6 @@ function loadAuthority() {
source.addEventListener("server-started", onServerStarted); source.addEventListener("server-started", onServerStarted);
source.addEventListener("server-stopped", onServerStopped); source.addEventListener("server-stopped", onServerStopped);
console.info("Swtiching to requests section");
$("section").hide();
$("section#requests").show();
$("#section-revoked").show();
$("#section-signed").show();
$("#section-requests").show();
$("#section-token").show();
} }
$("nav#menu li").click(function(e) { $("nav#menu li").click(function(e) {
@ -383,7 +394,7 @@ function loadAuthority() {
$(window).on("search", function() { $(window).on("search", function() {
var q = $("#search").val(); var q = $("#search").val();
$(".filterable").each(function(i, e) { $(".filterable").each(function(i, e) {
if ($(e).attr("data-cn").toLowerCase().indexOf(q) >= 0) { if ($(e).attr("data-keywords").toLowerCase().indexOf(q) >= 0) {
$(e).show(); $(e).show();
} else { } else {
$(e).hide(); $(e).hide();
@ -432,32 +443,53 @@ function loadAuthority() {
}); });
} }
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
$("#column-requests").show();
$("#column-signed").show();
$("#column-revoked").show();
$("#column-log").hide();
});
/** /**
* Fetch log entries * Fetch log entries
*/ */
if (session.features.logging) { if (session.features.logging) {
$("nav .nav-link.log").removeClass("disabled").click(function() { if (query.columns == 4) {
$("#view-dashboard").hide(); loadLog();
$("#view-log").show(); } else {
$.ajax({ $("nav .nav-link.log").removeClass("disabled").click(function() {
method: "GET", $("#column-requests").show();
url: "/api/log/", $("#column-signed").show();
dataType: "json", $("#column-revoked").show();
success: function(entries, status, xhr) { $("#column-log").hide();
console.info("Got", entries.length, "log entries");
console.info("j=", entries.length-1);
for (var j = entries.length-1; j--; ) {
onLogEntry(entries[j]);
};
source.addEventListener("log-entry", onLogEntry);
}
}); });
}); }
} }
} }
}); });
} }
function loadLog() {
if (window.log_initialized) return;
window.log_initialized = true;
$.ajax({
method: "GET",
url: "/api/log/",
dataType: "json",
success: function(entries, status, xhr) {
console.info("Got", entries.length, "log entries");
console.info("j=", entries.length-1);
for (var j = entries.length-1; j--; ) {
onLogEntry(entries[j]);
};
source.addEventListener("log-entry", onLogEntry);
$("#column-log .loader-container").hide();
$("#column-log .content").show();
}
});
}
function datetimeFilter(s) { function datetimeFilter(s) {
return new Date(s); return new Date(s);
} }

View File

@ -0,0 +1,19 @@
# Create VPN gateway up/down script for reporting client IP addresses to CA
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
#!/bin/sh
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
case $PLUTO_VERB in
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
*) ;;
esac
case $script_type in
client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;;
*) ;;
esac
EOF
chmod +x /etc/certidude/authority/{{ authority_name }}/updown

View File

@ -0,0 +1,27 @@
# Install packages on Ubuntu & Fedora
which apt && apt install openvpn
which dnf && dnf install openvpn
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
client
nobind
{% for router in session.service.routers %}
remote {{ router }} 1194 udp
remote {{ router }} 443 tcp-client
{% endfor %}
tls-version-min 1.2
tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384
cipher AES-128-GCM
auth SHA384
mute-replay-warnings
reneg-sec 0
remote-cert-tls server
dev tun
persist-tun
persist-key
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
EOF
systemctl restart openvpn

View File

@ -0,0 +1,89 @@
opkg update
opkg install curl openssl-util openvpn-openssl
{% if session.authority.certificate.algorithm != "ec" %}
# Generate Diffie-Hellman parameters file for OpenVPN
test -e /etc/certidude/dh.pem \
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
{% endif %}
# Create interface definition for tunnel
uci set network.vpn=interface
uci set network.vpn.name='vpn'
uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp
uci set network.vpn.proto='none'
# Create zone definition for VPN interface
uci set firewall.vpn=zone
uci set firewall.vpn.name='vpn'
uci set firewall.vpn.input='ACCEPT'
uci set firewall.vpn.forward='ACCEPT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.network='vpn'
# Allow UDP 1194 on WAN interface
uci set firewall.openvpn=rule
uci set firewall.openvpn.name='Allow OpenVPN'
uci set firewall.openvpn.src='wan'
uci set firewall.openvpn.dest_port=1194
uci set firewall.openvpn.proto='udp'
uci set firewall.openvpn.target='ACCEPT'
# Allow TCP 443 on WAN interface
uci set firewall.openvpn=rule
uci set firewall.openvpn.name='Allow OpenVPN over TCP'
uci set firewall.openvpn.src='wan'
uci set firewall.openvpn.dest_port=443
uci set firewall.openvpn.proto='tcp'
uci set firewall.openvpn.target='ACCEPT'
# Forward traffic from VPN to LAN
uci set firewall.c2s=forwarding
uci set firewall.c2s.src='vpn'
uci set firewall.c2s.dest='lan'
# Permit DNS queries from VPN
uci set dhcp.@dnsmasq[0].localservice='0'
touch /etc/config/openvpn
# Configure OpenVPN over TCP
uci set openvpn.s2c_tcp=openvpn
uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr)
uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128'
uci set openvpn.s2c_tcp.proto='tcp-server'
uci set openvpn.s2c_tcp.port='443'
uci set openvpn.s2c_tcp.dev=tun_s2c_tcp
# Configure OpenVPN over UDP
uci set openvpn.s2c_udp=openvpn
uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr)
uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128'
uci set openvpn.s2c_tcp.dev=tun_s2c_udp
for section in s2c_tcp s2c_udp; do
# Common paths
uci set openvpn.$section.script_security=2
uci set openvpn.$section.client_connect='/etc/certidude/updown'
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
uci set openvpn.$section.enabled=1
# DNS and routes
uci add_list openvpn.$section.push="route-metric 1000"
uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
# Security hardening
uci set openvpn.$section.tls_version_min='1.2'
uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384'
uci set openvpn.$section.cipher='AES-128-GCM'
uci set openvpn.$section.auth='SHA384'
done
/etc/init.d/openvpn restart
/etc/init.d/firewall restart

View File

@ -0,0 +1,7 @@
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'

View File

@ -0,0 +1,15 @@
test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
test -e /bin/hostname && NAME=$(hostname)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
{% include "snippets/update-trust.sh" %}
{% include "snippets/request-common.sh" %}
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'

View File

@ -0,0 +1,14 @@
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
{% include "snippets/store-authority.sh" %}
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem

View File

@ -0,0 +1,13 @@
test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
test -e /bin/hostname && NAME=$(hostname -f)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
{% include "snippets/update-trust.sh" %}
{% include "snippets/request-common.sh" %}
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'

View File

@ -0,0 +1,5 @@
mkdir -p /etc/certidude/authority/{{ authority_name }}/
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF

View File

@ -0,0 +1,29 @@
cat > /etc/ipsec.conf << EOF
ca {{ authority_name }}
auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
conn client-to-site
auto=start
right={{ session.service.routers[0] }}
rightsubnet=0.0.0.0/0
rightca="{{ session.authority.certificate.distinguished_name }}"
left=%defaultroute
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
leftsourceip=%config
leftca="{{ session.authority.certificate.distinguished_name }}"
keyexchange=ikev2
keyingtries=%forever
dpdaction=restart
closeaction=restart
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
ipsec restart

View File

@ -0,0 +1,16 @@
# Install packages on Ubuntu & Fedora, patch Fedora paths
which apt && apt install strongswan
which dnf && dnf install strongswan
test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
# Set SELinux context
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
# Patch AppArmor
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
/etc/certidude/authority/**
EOF

View File

@ -0,0 +1,41 @@
# Generate StrongSwan config
cat > /etc/ipsec.conf << EOF
config setup
strictcrlpolicy=yes
uniqueids=yes
ca {{ authority_name }}
auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
conn default-{{ authority_name }}
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftid={{ session.service.routers | first }}
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
leftca="{{ session.authority.certificate.distinguished_name }}"
rightca="{{ session.authority.certificate.distinguished_name }}"
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
dpddelay=0
dpdaction=clear
conn site-to-clients
auto=add
also=default-{{ authority_name }}
conn site-to-client1
auto=ignore
also=default-{{ authority_name }}
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
rightsourceip=172.21.0.1
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets

View File

@ -0,0 +1,7 @@
test -e /etc/pki/ca-trust/source/anchors \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
&& update-ca-trust
test -e /usr/local/share/ca-certificates/ \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
&& update-ca-certificates

View File

@ -0,0 +1,61 @@
# Install CA certificate
@"
{{ session.authority.certificate.blob }}
"@ | Out-File ca_cert.pem
{% if session.authority.certificate.algorithm == "ec" %}
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
{% else %}
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
{% endif %}
# Generate keypair and submit CSR
$hostname = $env:computername.ToLower()
@"
[NewRequest]
Subject = "CN=$hostname"
Exportable = FALSE
KeySpec = 1
KeyUsage = 0xA0
MachineKeySet = True
ProviderType = 12
RequestType = PKCS10
{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider"
KeyAlgorithm = ECDSA_P384
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
KeyLength = 2048
{% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
# Import certificate
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
{% endif %}
# Set up IPSec VPN tunnel
Remove-VpnConnection -AllUserConnection -Force k-space
Add-VpnConnection `
-Name k-space `
-ServerAddress guests.k-space.ee `
-AuthenticationMethod MachineCertificate `
-SplitTunneling `
-TunnelType ikev2 `
-PassThru -AllUserConnection
# Security hardening
Set-VpnConnectionIPsecConfiguration `
-ConnectionName k-space `
-AuthenticationTransformConstants GCMAES128 `
-CipherTransformConstants GCMAES128 `
-EncryptionMethod AES256 `
-IntegrityCheckMethod SHA384 `
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
-PassThru -AllUserConnection -Force
{#
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256
EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256
IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
PfsGroup = ESP key exchange, one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
#}

View File

@ -1,625 +1,162 @@
<div class="modal fade" id="request_submission_modal" role="dialog"> <div class="modal fade" id="request_submission_modal" role="dialog">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Request submission</h4> <h4 class="modal-title">Request submission</h4>
</div> </div>
<form action="/api/request/" method="post"> <form action="/api/request/" method="post">
<div class="modal-body"> <div class="modal-body">
{% if "ikev2" in session.service.protocols %} {% if "ikev2" in session.service.protocols %}
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5> <h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
<p>On Windows execute following PowerShell script</p> <p>On Windows execute following PowerShell script</p>
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
{% endif %}
<div class="highlight"> <h5>UNIX & UNIX-like</h5>
<pre class="code"><code># Install CA certificate <p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
@" <div class="highlight">
{{ session.authority.certificate.blob }}
"@ | Out-File ca_cert.pem
{% if session.authority.certificate.algorithm == "ec" %}
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
{% else %}
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
{% endif %}
# Generate keypair and submit CSR <pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre>
$hostname = $env:computername.ToLower() </div>
@"
[NewRequest]
Subject = "CN=$hostname"
Exportable = FALSE
KeySpec = 1
KeyUsage = 0xA0
MachineKeySet = True
ProviderType = 12
RequestType = PKCS10
{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider"
KeyAlgorithm = ECDSA_P384
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
KeyLength = 2048
{% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
# Import certificate <p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My <div class="highlight">
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem <pre class="code"><code>{% include "snippets/request-server.sh" %}</code></pre>
{% endif %} </div>
# Set up IPSec VPN tunnel
Remove-VpnConnection -AllUserConnection -Force k-space
Add-VpnConnection `
-Name k-space `
-ServerAddress guests.k-space.ee `
-AuthenticationMethod MachineCertificate `
-SplitTunneling `
-TunnelType ikev2 `
-PassThru -AllUserConnection
# Security hardening <p>To renew:</p>
Set-VpnConnectionIPsecConfiguration `
-ConnectionName k-space `
-AuthenticationTransformConstants GCMAES128 `
-CipherTransformConstants GCMAES128 `
-EncryptionMethod AES256 `
-IntegrityCheckMethod SHA384 `
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
-PassThru -AllUserConnection -Force</code></pre>
</div>
<!-- <div class="highlight">
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 <pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 </div>
EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256
IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
-->
{% endif %}
<h5>UNIX & UNIX-like</h5>
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
<div class="highlight">
<pre class="code"><code>test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
test -e /bin/hostname && NAME=$(hostname)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
mkdir -p /etc/certidude/authority/{{ authority_name }}/
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
test -e /etc/pki/ca-trust/source/anchors \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
&& update-ca-trust
test -e /usr/local/share/ca-certificates/ \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
&& update-ca-certificates
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
</code></pre>
</div>
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
<div class="highlight">
<pre class="code"><code>test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
test -e /bin/hostname && NAME=$(hostname -f)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
mkdir -p /etc/certidude/authority/{{ authority_name }}/
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
</code></pre>
</div>
<p>To renew:</p>
<div class="highlight">
<pre class="code"><code>curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
</code></pre>
</div>
{% if "openvpn" in session.service.protocols %}
{% if "openvpn" in session.service.protocols %}
<h5>OpenVPN as client</h5> <h5>OpenVPN as client</h5>
<p>First acquire certificates using the snippet above.</p> <p>First acquire certificates using the snippet above.</p>
<p>Then install software:</p> <p>Then install software:</p>
<div class="highlight"> <div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
<pre class="code"><code># Install packages on Ubuntu & Fedora {% endif %}
which apt && apt install openvpn
which dnf && dnf install openvpn
cat > /etc/openvpn/{{ authority_name }}.conf << EOF {% if "ikev2" in session.service.protocols %}
client
nobind
{% for router in session.service.routers %}
remote {{ router }} 1194 udp
remote {{ router }} 443 tcp-client
{% endfor %}
tls-version-min 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
cipher AES-128-GCM
auth SHA384
mute-replay-warnings
reneg-sec 0
remote-cert-tls server
dev tun
persist-tun
persist-key
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
EOF
systemctl restart openvpn
</code></pre>
</div>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<h5>StrongSwan as client</h5> <h5>StrongSwan as client</h5>
<p>First acquire certificates using the snippet above.</p> <p>First acquire certificates using the snippet above.</p>
<p>Then install software:</p> <p>Then install software:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths <pre class="code"><code>{% include "snippets/strongswan-patching.sh" %}</code></pre>
which apt && apt install strongswan
which dnf && dnf install strongswan
test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
# Hard link files to prevent Apparmor issues and have more manageable config
ln /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
ln /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
ln /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
</code></pre>
</div> </div>
<p>To configure StrongSwan as roadwarrior:</p> <p>To configure StrongSwan as roadwarrior:</p>
<div class="highlight"> <div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
<pre class="code"><code>cat > /etc/ipsec.conf << EOF {% endif %}
ca {{ authority_name }}
auto=add
cacert = {{ authority_name }}.pem
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
conn client-to-site
auto=start
right={{ session.service.routers[0] }}
rightsubnet=0.0.0.0/0
rightca="{{ session.authority.certificate.distinguished_name }}"
left=%defaultroute
leftcert={{ authority_name }}.pem
leftsourceip=%config
leftca="{{ session.authority.certificate.distinguished_name }}"
keyexchange=ikev2
keyingtries=%forever
dpdaction=restart
closeaction=restart
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac!
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
ipsec restart</code></pre>
</div>
{% endif %} <h5>OpenWrt/LEDE as VPN gateway</h5>
<p>First enroll certificates using the snippet from UNIX section above</p>
<h5>OpenWrt/LEDE as VPN gateway</h5> <p>Then:</p>
<div class="highlight">
<p>First enroll certificates using the snippet from UNIX section above</p> <pre class="code"><code>opkg install curl libmbedtls
# Derive FQDN from WAN interface's reverse DNS record
<p>Then:</p> FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
<div class="highlight"> grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
<pre class="code"><code>opkg install curl libmbedtls {% include "snippets/gateway-updown.sh" %}
# Derive FQDN from WAN interface's reverse DNS record </code></pre>
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) </div>
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
# Create VPN gateway up/down script for reporting client IP addresses to CA
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
#!/bin/sh
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
case $PLUTO_VERB in
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
*) ;;
esac
case $script_type in
client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;;
*) ;;
esac
EOF
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
</code></pre>
</div>
{% if "openvpn" in session.service.protocols %}
{% if "openvpn" in session.service.protocols %}
<p>Then either set up OpenVPN service:</p> <p>Then either set up OpenVPN service:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>opkg update <pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
opkg install curl openssl-util openvpn-openssl
{% if session.authority.certificate.algorithm != "ec" %}
# Generate Diffie-Hellman parameters file for OpenVPN
test -e /etc/certidude/dh.pem \
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
{% endif %}
# Create interface definition for tunnel
uci set network.vpn=interface
uci set network.vpn.name='vpn'
uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp
uci set network.vpn.proto='none'
# Create zone definition for VPN interface
uci set firewall.vpn=zone
uci set firewall.vpn.name='vpn'
uci set firewall.vpn.input='ACCEPT'
uci set firewall.vpn.forward='ACCEPT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.network='vpn'
# Allow UDP 1194 on WAN interface
uci set firewall.openvpn=rule
uci set firewall.openvpn.name='Allow OpenVPN'
uci set firewall.openvpn.src='wan'
uci set firewall.openvpn.dest_port=1194
uci set firewall.openvpn.proto='udp'
uci set firewall.openvpn.target='ACCEPT'
# Allow TCP 443 on WAN interface
uci set firewall.openvpn=rule
uci set firewall.openvpn.name='Allow OpenVPN over TCP'
uci set firewall.openvpn.src='wan'
uci set firewall.openvpn.dest_port=443
uci set firewall.openvpn.proto='tcp'
uci set firewall.openvpn.target='ACCEPT'
# Forward traffic from VPN to LAN
uci set firewall.c2s=forwarding
uci set firewall.c2s.src='vpn'
uci set firewall.c2s.dest='lan'
# Permit DNS queries from VPN
uci set dhcp.@dnsmasq[0].localservice='0'
touch /etc/config/openvpn
# Configure OpenVPN over TCP
uci set openvpn.s2c_tcp=openvpn
uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr)
uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128'
uci set openvpn.s2c_tcp.proto='tcp-server'
uci set openvpn.s2c_tcp.port='443'
uci set openvpn.s2c_tcp.dev=tun_s2c_tcp
# Configure OpenVPN over UDP
uci set openvpn.s2c_udp=openvpn
uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr)
uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128'
uci set openvpn.s2c_tcp.dev=tun_s2c_udp
for section in s2c_tcp s2c_udp; do
# Common paths
uci set openvpn.$section.script_security=2
uci set openvpn.$section.client_connect='/etc/certidude/updown'
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
uci set openvpn.$section.enabled=1
# DNS and routes
uci add_list openvpn.$section.push="route-metric 1000"
uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
# Security hardening
uci set openvpn.$section.tls_version_min='1.2'
uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384'
uci set openvpn.$section.cipher='AES-128-GCM'
uci set openvpn.$section.auth='SHA384'
done
/etc/init.d/openvpn restart
/etc/init.d/firewall restart</code></pre>
</div> </div>
{% endif %}
{% endif %} {% if "ikev2" in session.service.protocols %}
{% if "ikev2" in session.service.protocols %}
<p>Alternatively or additionally set up StrongSwan:</p> <p>Alternatively or additionally set up StrongSwan:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>opkg update <pre class="code"><code>opkg update
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
{% include "snippets/strongswan-server.sh" %}
# Generate StrongSwan config ipsec restart</code></pre>
cat > /etc/ipsec.conf << EOF
config setup
strictcrlpolicy=yes
uniqueids = yes
ca {{ authority_name }}
auto=add
cacert = {{ authority_name }}.pem
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
conn default-{{ authority_name }}
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac!
left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftid={{ session.service.routers | first }}
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
leftcert={{ authority_name }}.pem
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
leftca="{{ session.authority.certificate.distinguished_name }}"
rightca="{{ session.authority.certificate.distinguished_name }}"
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
dpddelay=0
dpdaction=clear
conn site-to-clients
auto=add
also=default-{{ authority_name }}
conn site-to-client1
auto=ignore
also=default-{{ authority_name }}
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
rightsourceip=172.21.0.1
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
ipsec restart</code></pre>
</div> </div>
{% endif %} {% endif %}
{% if session.authority.builder %} {% if session.authority.builder %}
<h5>OpenWrt/LEDE image builder</h5> <h5>OpenWrt/LEDE image builder</h5>
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p> <p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
<ul> <ul>
{% for name, title, filename in session.authority.builder.profiles %} {% for name, title, filename in session.authority.builder.profiles %}
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li> <li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<h5>SCEP</h5> <h5>SCEP</h5>
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p> <p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
<h5>Copy & paste</h5> <h5>Copy & paste</h5>
<p>Use whatever tools you have available on your platform to generate <p>Use whatever tools you have available on your platform to generate
keypair and just paste ASCII armored PEM file contents here and hit submit:</p> keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
<textarea id="request_body" style="width:100%; min-height: 10em;" placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea> <textarea id="request_body" style="width:100%; min-height: 10em;"
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
</div>
<div class="modal-footer">
<div class="btn-group">
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
</div> </div>
<div class="modal-footer"> </div>
<div class="btn-group"> </form>
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div>
<div class="modal fade" id="revocation_list_modal" role="dialog"> <div class="modal fade" id="revocation_list_modal" role="dialog">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Revocation lists</h4> <h4 class="modal-title">Revocation lists</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p> <p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der <pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file" curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre> curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" data-dismiss="modal">Close</button> <button type="button" class="btn" data-dismiss="modal">Close</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<section id="about">
<h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
<p title="Bundles are mainly intended for Android and iOS users">
Click <button id="enroll">here</button> to generate Android or iOS bundle for current user account.</p>
<p>Mails will be sent to: {{ session.user.mail }}</p>
{% if session.authority %}
<h2>Authority certificate</h2>
<p>Several things are hardcoded into the <a href="/api/certificate">certificate</a> and
as such require complete reset of X509 infrastructure if some of them needs to be changed.</p>
<h2>Authority settings</h2>
<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>
{% if session.authority.mailer %}
<p>Mails will appear from: {{ session.authority.mailer.name }} &lt;{{ session.authority.mailer.address }}&gt;</p>
{% else %}
<p>E-mail disabled</p>
{% endif %}
<p>User enrollment:
{% if session.authority.user_enrollment_allowed %}
{% if session.authority.user_multiple_certificates %}
multiple
{% else %}
single
{% endif %}
allowed
{% else %}
forbidden
{% endif %}
</p>
<p>Machine enrollment:
{% if session.authority.machine_enrollment_allowed %}
allowed
{% else %}
forbidden
{% endif %}
</p>
<p>Certificate attributes:</p>
<ul>
<li>Server certificate lifetime: {{ session.authority.signature.server_certificate_lifetime }} days</li>
<li>Client certificate lifetime: {{ session.authority.signature.client_certificate_lifetime }} days</li>
{% if session.features.crl %}
<li>Revocation list lifetime: {{ session.authority.signature.revocation_list_lifetime }} seconds</li>
{% endif %}
</ul>
<p>Authenticated users allowed from:
{% if not session.authority.user_subnets %}
nowhere</p>
{% elif "0.0.0.0/0" in session.authority.user_subnets %}
anywhere</p>
{% else %}
</p>
<ul>
{% for i in session.authority.user_subnets %}
<li>{{ i }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Authority administration is allowed from:
{% if not session.authority.admin_subnets %}
nowhere</p>
{% elif "0.0.0.0/0" in session.authority.admin_subnets %}
anywhere</p>
{% else %}
<ul>
{% for subnet in session.authority.admin_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>Authority administration allowed for:</p>
<ul>
{% for user in session.authority.admin_users %}
<li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li>
{% endfor %}
</ul>
</section>
{% else %}
<p>Here you can renew your certificates</p>
{% endif %}
{% set s = session.certificate.identity %}
<div class="row"> <div class="row">
<div class="col-sm-{{ column_width }}">
<div class="col-sm-6">
<h1>Signed certificates</h1> <h1>Signed certificates</h1>
<p>Following certificates have been signed:</p> <p>Authority administration allowed for
{% for user in session.authority.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authority.admin_subnets %}anywhere{% else %}
{% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
Following certificates have been signed:</p>
<div id="signed_certificates"> <div id="signed_certificates">
{% for certificate in session.authority.signed | sort(attribute="signed", reverse=true) %} {% for certificate in session.authority.signed | sort(attribute="signed", reverse=true) %}
{% include "views/signed.html" %} {% include "views/signed.html" %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-{{ column_width }}">
{% if session.authority %}
{% if session.authority %}
{% if session.features.token %} {% if session.features.token %}
<h1>Tokens</h1> <h1>Tokens</h1>
<p>Tokens allow enrolling smartphones and third party devices.</p> <p>Tokens allow enrolling smartphones and third party devices.</p>
<ul> <ul>
<li>You can issue yourself a token to be used on a mobile device</li> <li>You can issue yourself a token to be used on a mobile device</li>
@ -635,7 +172,6 @@ forbidden
</span> </span>
</div> </div>
</p> </p>
<div id="token_qrcode"></div> <div id="token_qrcode"></div>
{% endif %} {% endif %}
@ -649,30 +185,32 @@ forbidden
Request submission is enabled. Request submission is enabled.
{% else %} {% else %}
Request submission allowed from Request submission allowed from
{% for subnet in session.authority.request_subnets %}{{ subnet }},{% endfor %}. {% for subnet in session.authority.request_subnets %}
{{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %}.
{% endif %} {% endif %}
{# if session.request_submission_allowed #}
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
{# endif #}
{% if session.authority.autosign_subnets %} {% if session.authority.autosign_subnets %}
{% if "0.0.0.0/0" in session.authority.autosign_subnets %} {% if "0.0.0.0/0" in session.authority.autosign_subnets %}
All requests are automatically signed. All requests are automatically signed.
{% else %} {% else %}
Requests from Requests from
{% for subnet in session.authority.autosign_subnets %} {% for subnet in session.authority.autosign_subnets %}
{{ subnet }}, {{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %} {% endfor %}
are automatically signed. are automatically signed.
{% endif %} {% endif %}
{% endif %} {% endif %}
</p> </p>
{% if columns >= 3 %}
</div>
<div class="col-sm-{{ column_width }}">
{% endif %}
<div id="pending_requests"> <div id="pending_requests">
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
{% include "views/request.html" %} {% include "views/request.html" %}
{% endfor %} {% endfor %}
</div> </div>
<p><h1>Revoked certificates</h1></p> <p><h1>Revoked certificates</h1></p>
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click <p>Following certificates have been revoked{% if session.features.crl %}, for more information click
@ -682,8 +220,24 @@ forbidden
{% include "views/revoked.html" %} {% include "views/revoked.html" %}
{% endfor %} {% endfor %}
</div> </div>
<div id="column-log" class="col-sm-{% if columns == 4 %}{{ column_width }}{% else %}12{% endif %}" {% if columns < 4 %}style="display:none;"{% endif %}>
<div class="loader-container">
<div class="loader"></div>
<p>Loading logs, this might take a while...</p>
</div>
<div class="content" style="display:none;">
<h1>Log</h1>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked> Critical</label>
<label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label>
<label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label>
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</label>
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off"> Debug</label>
</div>
<ul id="log-entries" class="list-group">
</ul>
</div>
</div>
</div> </div>
<section id="config">
</section>
{% endif %} {% endif %}

View File

@ -0,0 +1,14 @@
<p>You're viewing this page over insecure channel.
You can give it a try and <a href="https://{{ authority_name }}">connect over HTTPS</a>,
if that succeeds all subsequents accesses of this page will go over HTTPS.
</p>
<p>
Click <a href="/api/certificate">here</a> to fetch the certificate of this authority.
Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet:
</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/store-authority.sh" %}
{% include "snippets/update-trust.sh" %}</code></pre>
</div>

View File

@ -1,8 +1,8 @@
<i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 3600 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/> <i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 7200 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/>
Last seen Last seen
<time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
at at
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %} <a target="_blank" href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
from from
<a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}. <a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.

View File

@ -1,5 +1,5 @@
<p> <div id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
<div id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="card"> data-keywords="{{ request.common_name }}|">
<div class="card-header"> <div class="card-header">
{% if certificate.server %} {% if certificate.server %}
<i class="fa fa-server"></i> <i class="fa fa-server"></i>
@ -62,4 +62,3 @@ curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}
</div> </div>
</div> </div>
</div> </div>
</p>

View File

@ -1,4 +1,5 @@
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card"> <div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
data-keywords="{{ certificate.common_name }}|">
<div class="card-body"> <div class="card-body">
<div class="card-header"> <div class="card-header">
{% if certificate.server %} {% if certificate.server %}
@ -62,10 +63,7 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
</tbody> </tbody>
</table> </table>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<br/>

View File

@ -1,5 +1,5 @@
<p> <div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card"> data-keywords="{{ certificate.common_name }}|{% if session.authority.tagging %}{% for tag in certificate.tags %}{{ tag.id }}|{% endfor %}{% endif %}{% for key, value in certificate.attributes %}{{ key }}={{ value }}|{% endfor %}">
<div class="card-header"> <div class="card-header">
{% if certificate.organizational_unit %} {% if certificate.organizational_unit %}
<i class="fa fa-folder" aria-hidden="true"></i> <i class="fa fa-folder" aria-hidden="true"></i>
@ -121,4 +121,3 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
</div> </div>
</div> </div>
</div> </div>
</p>

View File

View File

@ -2,5 +2,5 @@
<span data-cn="{{ certificate.common_name }}" <span data-cn="{{ certificate.common_name }}"
title="{{ tag.id }}" title="{{ tag.id }}"
class="badge badge-default" class="badge badge-default"
onClick="onTagClicked(this);"><i class="fa fa-{{ tag.key }}"></i> {{ tag.value }}</span> onClick="onTagClicked(this);">{{ tag.value }}</span>
{% endfor %} {% endfor %}

View File

@ -28,7 +28,7 @@ if [ -e /sys/class/dmi ]; then
ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB" ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB"
else else
ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)" ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)"
ARGS="$ARGS&mem=$(echo $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1)/1000+1 | bc) MB" ARGS="$ARGS&mem=$(expr $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1) / 1024 + 1 ) MB"
fi fi
# Submit some stats to CA # Submit some stats to CA

View File

@ -9,6 +9,13 @@ router = ^router\d?\.
# use it to include SSH keys, set passwords, etc # use it to include SSH keys, set passwords, etc
script = script =
# Which subnets are routed to the tunnel
subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
# Strongswan IKEv2 proposals
ike=aes256-sha384-{{ dhgroup }}!
esp=aes128gcm16-aes128gmac-{{ dhgroup }}!
[tpl-archer-c7] [tpl-archer-c7]
# Title shown in the UI # Title shown in the UI
title = TP-Link Archer C7 (Access Point) title = TP-Link Archer C7 (Access Point)

View File

@ -8,7 +8,6 @@
limit_conn addr 10; limit_conn addr 10;
client_body_timeout 5s; client_body_timeout 5s;
client_header_timeout 5s; client_header_timeout 5s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn_zone $binary_remote_addr zone=addr:10m;
# Backend configuration # Backend configuration
@ -23,19 +22,10 @@ send_timeout 600;
# Don't buffer any messages # Don't buffer any messages
nchan_message_buffer_length 0; nchan_message_buffer_length 0;
# To use CA-s own certificate for HTTPS # To use CA-s own certificate for frontend and mutually authenticated connections
ssl_certificate /var/lib/certidude/{{ common_name }}/signed/{{ common_name }}.pem; ssl_certificate /var/lib/certidude/{{ common_name }}/signed/{{ common_name }}.pem;
ssl_certificate_key /var/lib/certidude/{{common_name}}/self_key.pem; ssl_certificate_key /var/lib/certidude/{{common_name}}/self_key.pem;
# To use Let's Encrypt certificates
#ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem;
#ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem;
# Also run the following to set up Let's Encrypt certificates:
#
# apt install letsencrypt
# certbot certonly -d {{common_name}} --webroot /var/www/html/
server { server {
# Section for serving insecure HTTP, note that this is suitable for # Section for serving insecure HTTP, note that this is suitable for
# OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms. # OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms.
@ -47,7 +37,6 @@ server {
# Proxy pass to backend # Proxy pass to backend
location /api/ { location /api/ {
proxy_pass http://127.0.1.1:8080/api/; proxy_pass http://127.0.1.1:8080/api/;
limit_req zone=api burst=5;
} }
# Path to static files # Path to static files
@ -90,6 +79,15 @@ server {
listen 443 ssl http2 default_server; listen 443 ssl http2 default_server;
server_name {{ common_name }}; server_name {{ common_name }};
# To use Let's Encrypt certificates
{% if not letsencrypt %}#{% endif %}ssl_certificate {{ letsencrypt_fullchain }};
{% if not letsencrypt %}#{% endif %}ssl_certificate_key {{ letsencrypt_privkey }};
# Also run the following to set up Let's Encrypt certificates:
#
# apt install letsencrypt
# certbot certonly -d {{common_name}} --webroot /var/www/html/
# HSTS header below should make sure web interface will be accessed over HTTPS only # HSTS header below should make sure web interface will be accessed over HTTPS only
# once it has been configured # once it has been configured
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
@ -97,7 +95,6 @@ server {
# Proxy pass to backend # Proxy pass to backend
location /api/ { location /api/ {
proxy_pass http://127.0.1.1:8080/api/; proxy_pass http://127.0.1.1:8080/api/;
limit_req zone=api burst=5;
} }
# Path to static files # Path to static files
@ -146,7 +143,6 @@ server {
# Proxy pass to backend # Proxy pass to backend
location /api/ { location /api/ {
proxy_pass http://127.0.1.1:8080/api/; proxy_pass http://127.0.1.1:8080/api/;
limit_req zone=api burst=5;
} }
# Long poll # Long poll

View File

@ -82,6 +82,7 @@ class DirectoryConnection(object):
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
self.conn.unbind_s() self.conn.unbind_s()
del os.environ["KRB5CCNAME"] # prevent contaminating environment
class ActiveDirectoryUserManager(object): class ActiveDirectoryUserManager(object):

View File

@ -2,7 +2,7 @@
source common.sh source common.sh
uci set certidude.@authority[0].trigger=lan sed -e 's/trigger wan/trigger lan/' -i $OVERLAY/etc/config/certidude
cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname
@ -108,11 +108,11 @@ esac
EOF EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \
openssl-util curl ca-certificates dropbear \ openssl-util curl ca-certificates dropbear \
strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \ htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
-luci-app-firewall \ -luci-app-firewall \
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6 bc" -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"

View File

@ -45,19 +45,12 @@ config authority
EOF EOF
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
/etc/init.d/ipsec disable
EOF
case $AUTHORITY_CERTIFICATE_ALGORITHM in case $AUTHORITY_CERTIFICATE_ALGORITHM in
rsa) rsa)
echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
DHGROUP=modp2048
;; ;;
ec) ec)
echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
DHGROUP=ecp384
;; ;;
*) *)
echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM" echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM"
@ -96,8 +89,8 @@ conn %default
keyingtries=%forever keyingtries=%forever
dpdaction=restart dpdaction=restart
closeaction=restart closeaction=restart
ike=aes256-sha384-ecp384! ike=$IKE
esp=aes128gcm16-aes128gmac! esp=$ESP
left=%defaultroute left=%defaultroute
leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem
leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME" leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
@ -106,7 +99,7 @@ conn %default
conn client-to-site conn client-to-site
auto=start auto=start
right="$ROUTER" right="$ROUTER"
rightsubnet=0.0.0.0/0 rightsubnet="$SUBNETS"
leftsourceip=%config leftsourceip=%config
leftupdown=/etc/certidude/authority/$AUTHORITY/updown leftupdown=/etc/certidude/authority/$AUTHORITY/updown

View File

@ -41,4 +41,4 @@ EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \ strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \ iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun bc" pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun"

View File

@ -7,4 +7,4 @@ AUTHORITY=certidude.@authority[0]
[ $ACTION == "ifup" ] || exit 0 [ $ACTION == "ifup" ] || exit 0
[ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0 [ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0
/usr/bin/certidude-enroll /usr/bin/certidude-enroll > /var/log/certidude.log 2>&1

View File

@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
set -e
set -x
AUTHORITY=certidude.@authority[0] AUTHORITY=certidude.@authority[0]
# TODO: iterate over all authorities # TODO: iterate over all authorities
@ -29,7 +32,6 @@ logger -t certidude -s "Time is now: $(date)"
if [ -f $CERTIFICATE_PATH ]; then if [ -f $CERTIFICATE_PATH ]; then
SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f]) SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f])
logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..." logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..."
ipsec restart
exit 0 exit 0
fi fi
@ -39,16 +41,20 @@ fi
######################################### #########################################
if [ ! -f $KEY_PATH ]; then if [ ! -f $KEY_PATH ]; then
logger -t certidude -s "Generating $KEY_TYPE key for VPN..."
case $KEY_TYPE in case $KEY_TYPE in
rsa) rsa)
logger -t certidude -s "Generating $KEY_LENGTH-bit RSA key..."
openssl genrsa -out $KEY_PATH.part $KEY_LENGTH openssl genrsa -out $KEY_PATH.part $KEY_LENGTH
openssl rsa -in $KEY_PATH.part -noout
;; ;;
ec) ec)
logger -t certidude -s "Generating $KEY_CURVE ECDSA key..."
openssl ecparam -name $KEY_CURVE -genkey -noout -out $KEY_PATH.part openssl ecparam -name $KEY_CURVE -genkey -noout -out $KEY_PATH.part
;; ;;
*)
logger -t certidude -s "Unsupported key type $KEY_TYPE"
exit 255
;;
esac esac
mv $KEY_PATH.part $KEY_PATH mv $KEY_PATH.part $KEY_PATH
fi fi
@ -120,4 +126,5 @@ mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
# Start services # Start services
logger -t certidude -s "Starting IPSec IKEv2 daemon..." logger -t certidude -s "Starting IPSec IKEv2 daemon..."
ipsec restart /etc/init.d/ipsec enable
/etc/init.d/ipsec restart

View File

@ -49,7 +49,7 @@ def client():
def generate_csr(cn=None): def generate_csr(cn=None):
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048) public_key, private_key = asymmetric.generate_pair('ec', curve="secp384r1")
builder = CSRBuilder({ 'common_name': cn }, public_key) builder = CSRBuilder({ 'common_name': cn }, public_key)
request = builder.build(private_key) request = builder.build(private_key)
return pem_armor_csr(request) return pem_armor_csr(request)
@ -116,6 +116,10 @@ def clean_server():
"/etc/nginx/sites-enabled/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf",
"/etc/nginx/conf.d/tls.conf", "/etc/nginx/conf.d/tls.conf",
"/etc/certidude/server.keytab", "/etc/certidude/server.keytab",
"/tmp/sscep/ca.pem",
"/tmp/key.pem",
"/tmp/req.pem",
"/tmp/cert.pem",
] ]
for filename in files: for filename in files:
@ -142,14 +146,19 @@ def clean_server():
# Restore initial resolv.conf # Restore initial resolv.conf
shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf") shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf")
def assert_cleanliness():
assert os.getuid() == 0, "Environment contaminated, UID: %d" % os.getuid()
assert os.getgid() == 0, "Environment contaminated, GID: %d" % os.getgid()
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated, KRB5_KTNAME=%s" % os.environ.get("KRB5_KTNAME")
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated, KRB5CCNAME=%s" % os.environ.get("KRB5CCNAME")
def test_cli_setup_authority(): def test_cli_setup_authority():
assert os.getuid() == 0, "Run tests as root in a clean VM or container" assert os.getuid() == 0, "Run tests as root in a clean VM or container"
assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan" assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan"
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev") os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user winbind bc")
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated" assert_cleanliness()
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
# Mock Fedora # Mock Fedora
for util in "/usr/bin/chcon", "/usr/bin/dnf", "/usr/bin/update-ca-trust", "/usr/sbin/dmidecode": for util in "/usr/bin/chcon", "/usr/bin/dnf", "/usr/bin/update-ca-trust", "/usr/sbin/dmidecode":
@ -196,34 +205,23 @@ def test_cli_setup_authority():
assert const.HOSTNAME == "ca" assert const.HOSTNAME == "ca"
assert const.DOMAIN == "example.lan" assert const.DOMAIN == "example.lan"
# Bootstrap authority os.system("certidude setup authority --elliptic-curve")
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
if not bootstrap_pid:
assert os.getuid() == 0 and os.getgid() == 0
result = runner.invoke(cli, ["setup", "authority"])
assert not result.exception, result.output
return
else:
os.waitpid(bootstrap_pid, 0)
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated" assert_cleanliness()
# Make sure nginx is running # Make sure nginx is running
assert os.system("nginx -t") == 0, "invalid nginx configuration" assert os.system("nginx -t") == 0, "invalid nginx configuration"
os.system("service nginx restart")
assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly" assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly"
# Make sure we generated legit CA certificate # Make sure we generated legit CA certificate
from certidude import config, authority, auth, user from certidude import config, authority, user
assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000 assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000
assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000) assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
assert authority.server_flags("lauri@fedora-123") == False assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert authority.server_flags("fedora-123") == False assert authority.public_key.algorithm == "ec"
assert authority.server_flags("vpn.example.lan") == True
assert authority.server_flags("lauri@a.b.c") == False
# Generate garbage # Generate garbage
with open("/var/lib/certidude/ca.example.lan/bla", "w") as fh: with open("/var/lib/certidude/ca.example.lan/bla", "w") as fh:
@ -237,19 +235,18 @@ def test_cli_setup_authority():
# Start server before any signing operations are performed # Start server before any signing operations are performed
config.CERTIFICATE_RENEWAL_ALLOWED = True config.CERTIFICATE_RENEWAL_ALLOWED = True
assert_cleanliness()
server_pid = os.fork() import requests
if not server_pid: for j in range(0,10):
# Fork to prevent umask, setuid, setgid side effects r = requests.get("http://ca.example.lan/api/")
result = runner.invoke(cli, ['serve']) if r.status_code != 502:
assert not result.exception, result.output break
return sleep(1)
assert r.status_code == 401, "Timed out starting up the API backend"
sleep(1) # Wait for serve to start up
# TODO: check that port 8080 is listening, otherwise app probably crashed # TODO: check that port 8080 is listening, otherwise app probably crashed
import requests
# Test CA certificate fetch # Test CA certificate fetch
buf = open("/var/lib/certidude/ca.example.lan/ca_cert.pem").read() buf = open("/var/lib/certidude/ca.example.lan/ca_cert.pem").read()
@ -621,10 +618,7 @@ def test_cli_setup_authority():
assert r.status_code == 415 # invalid media type assert r.status_code == 415 # invalid media type
r = client().simulate_get("/api/", headers={"Authorization":usertoken}) r = client().simulate_get("/api/", headers={"Authorization":usertoken})
assert r.status_code == 200 assert r.status_code == 403 # regular users have no access
assert r.headers.get('content-type').startswith("application/json")
assert r.json, r.text
assert not r.json.get("authority"), r.text # No permissions to admin
r = client().simulate_get("/api/", headers={"Authorization":admintoken}) r = client().simulate_get("/api/", headers={"Authorization":admintoken})
assert r.status_code == 200 assert r.status_code == 200
@ -665,7 +659,6 @@ def test_cli_setup_authority():
assert not result.exception, result.output # client conf already exists, remove to regenerate assert not result.exception, result.output # client conf already exists, remove to regenerate
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
fh.write("autosign = false\n") fh.write("autosign = false\n")
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem") assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
@ -721,7 +714,6 @@ def test_cli_setup_authority():
assert not result.exception, result.output # client conf already exists, remove to regenerate assert not result.exception, result.output # client conf already exists, remove to regenerate
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
fh.write("autosign = false\n") fh.write("autosign = false\n")
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem") assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
@ -761,9 +753,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "roadwarrior1", "ca.example.lan", "vpn.example.lan"]) result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "roadwarrior1", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output # client conf already exists, remove to regenerate assert not result.exception, result.output # client conf already exists, remove to regenerate
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
assert not result.exception, result.output assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
@ -778,9 +767,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"]) result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
assert not result.exception, result.output assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
@ -947,7 +933,6 @@ def test_cli_setup_authority():
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
fh.write("autosign = false\n") fh.write("autosign = false\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
@ -985,9 +970,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'strongswan', 'client', "-cn", "roadwarrior2", "ca.example.lan", "ipsec.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'client', "-cn", "roadwarrior2", "ca.example.lan", "ipsec.example.lan"])
assert not result.exception, result.output # client conf already exists, remove to regenerate assert not result.exception, result.output # client conf already exists, remove to regenerate
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
assert not result.exception, result.output assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
@ -1001,9 +983,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'strongswan', 'networkmanager', "-cn", "roadwarrior4", "ca.example.lan", "ipsec.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'networkmanager', "-cn", "roadwarrior4", "ca.example.lan", "ipsec.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
assert not result.exception, result.output assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
@ -1075,15 +1054,9 @@ def test_cli_setup_authority():
### Switch to Kerberos/LDAP auth ### ### Switch to Kerberos/LDAP auth ###
#################################### ####################################
# Shut down current instance os.system("systemctl stop certidude")
os.kill(server_pid, 15)
requests.get("http://ca.example.lan/api/")
# sleep(2)
# os.kill(server_pid, 9) # TODO: Figure out why doesn't shut down gracefully
os.waitpid(server_pid, 0)
# Install packages # Install packages
os.system("apt-get install -y samba krb5-user winbind bc")
clean_server() clean_server()
# Bootstrap domain controller here, # Bootstrap domain controller here,
@ -1114,17 +1087,6 @@ def test_cli_setup_authority():
else: else:
assert False, "Samba startup timed out" assert False, "Samba startup timed out"
# Bootstrap authority
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
if not bootstrap_pid:
result = runner.invoke(cli, ["setup", "authority", "--skip-packages", "--elliptic-curve"])
assert not result.exception, result.output
return
else:
os.waitpid(bootstrap_pid, 0)
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated"
# (re)auth against DC # (re)auth against DC
assert os.system("kdestroy") == 0 assert os.system("kdestroy") == 0
assert not os.path.exists("/tmp/krb5cc_0") assert not os.path.exists("/tmp/krb5cc_0")
@ -1144,45 +1106,55 @@ def test_cli_setup_authority():
else: else:
os.waitpid(spn_pid, 0) os.waitpid(spn_pid, 0)
assert_cleanliness()
r = requests.get("http://ca.example.lan/api/") r = requests.get("http://ca.example.lan/api/")
assert r.status_code == 502, r.text assert r.status_code == 502, r.text
# Bootstrap authority
assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem")
os.system("certidude setup authority --skip-packages")
# Make modifications to /etc/certidude/server.conf so # Make modifications to /etc/certidude/server.conf so
# Certidude would auth against domain controller # Certidude would auth against domain controller
os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
from certidude.common import pip
# Update server credential cache # Update server credential cache
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
with open("/etc/cron.hourly/certidude") as fh: with open("/etc/cron.hourly/certidude") as fh:
cronjob = fh.read() cronjob = fh.read()
assert "ldap/ca.example.lan" in cronjob, cronjob assert "ldap/ca.example.lan" in cronjob, cronjob
os.system("/etc/cron.hourly/certidude") assert os.system("/etc/cron.hourly/certidude") == 0
assert os.path.exists("/run/certidude/krb5cc")
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
server_pid = os.fork() # Fork to prevent environment contamination # Start certidude backend
if not server_pid: assert os.system("systemctl restart certidude") == 0
# Apply /etc/certidude/server.conf changes assert_cleanliness()
reload(config)
reload(user)
reload(auth)
assert isinstance(user.User.objects, user.ActiveDirectoryUserManager), user.User.objects
result = runner.invoke(cli, ['users']) # Apply /etc/certidude/server.conf changes
assert not result.exception, result.output reload(config)
assert "user;userbot;User;Bot;userbot@example.lan" in result.output reload(user)
assert "admin;adminbot;Admin;Bot;adminbot@example.lan" in result.output reload(authority)
assert "admin;Administrator;Administrator;;Administrator@example.lan" in result.output
assert authority.public_key.algorithm == "rsa"
assert isinstance(user.User.objects, user.ActiveDirectoryUserManager), user.User.objects
result = runner.invoke(cli, ['users'])
assert not result.exception, result.output
assert "user;userbot;User;Bot;userbot@example.lan" in result.output
assert "admin;adminbot;Admin;Bot;adminbot@example.lan" in result.output
assert "admin;Administrator;Administrator;;Administrator@example.lan" in result.output
result = runner.invoke(cli, ['serve'])
assert not result.exception, result.output
return
# Wait for serve to start up # Wait for serve to start up
for j in range(0,10): for j in range(0,10):
@ -1215,10 +1187,15 @@ def test_cli_setup_authority():
### Kerberos auth ### ### Kerberos auth ###
##################### #####################
# TODO: pip install requests-kerberos # TODO: pip3 install requests-kerberos
assert_cleanliness()
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
from requests_kerberos import HTTPKerberosAuth, OPTIONAL from requests_kerberos import HTTPKerberosAuth, OPTIONAL
auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
# Test Kerberos auth # Test Kerberos auth
r = requests.get("http://ca.example.lan/api/") r = requests.get("http://ca.example.lan/api/")
assert r.status_code == 401, r.text assert r.status_code == 401, r.text
@ -1229,6 +1206,8 @@ def test_cli_setup_authority():
r = requests.get("http://ca.example.lan/api/", headers={"Authorization": "Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKADk4AAAADw=="}) r = requests.get("http://ca.example.lan/api/", headers={"Authorization": "Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKADk4AAAADw=="})
assert r.status_code == 400, r.text assert r.status_code == 400, r.text
assert "Unsupported authentication mechanism (NTLM" in r.text assert "Unsupported authentication mechanism (NTLM" in r.text
assert os.system("echo S4l4k4l4 | kinit administrator") == 0
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
r = requests.get("http://ca.example.lan/api/", auth=auth) r = requests.get("http://ca.example.lan/api/", auth=auth)
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
@ -1247,14 +1226,15 @@ def test_cli_setup_authority():
# curl http://ca.example.lan/api/ -u adminbot:S4l4k4l4 -H "User-agent: Android" -H "Referer: http://ca.example.lan" # curl http://ca.example.lan/api/ -u adminbot:S4l4k4l4 -H "User-agent: Android" -H "Referer: http://ca.example.lan"
r = requests.get("http://ca.example.lan/api/", r = requests.get("http://ca.example.lan/api/",
headers={"Authorization":usertoken, "User-Agent": "Android", "Referer":"http://ca.example.lan/"}) headers={"Authorization":usertoken, "User-Agent": "Android", "Referer":"http://ca.example.lan/"})
#assert r.status_code == 200, r.text # TODO: Fails with 500 in Travis assert r.status_code == 400, r.text
assert "expected Negotiate" in r.text, r.text
########################### ###########################
### Machine keytab auth ### ### Machine keytab auth ###
########################### ###########################
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated" assert_cleanliness()
mach_pid = os.fork() # Otherwise results in Terminated, needs investigation why mach_pid = os.fork() # Otherwise results in Terminated, needs investigation why
if not mach_pid: if not mach_pid:
@ -1264,9 +1244,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "somethingelse", "ca.example.lan", "vpn.example.lan"]) result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "somethingelse", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"])
assert result.exception, result.output # Bad request 400 assert result.exception, result.output # Bad request 400
@ -1276,9 +1253,6 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "ca", "ca.example.lan", "vpn.example.lan"]) result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "ca", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"])
assert not result.exception, result.output assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output assert "Writing certificate to:" in result.output, result.output
@ -1291,6 +1265,8 @@ def test_cli_setup_authority():
### SCEP tests ### ### SCEP tests ###
################## ##################
assert not os.path.exists("/tmp/sscep/ca.pem")
if not os.path.exists("/tmp/sscep"): if not os.path.exists("/tmp/sscep"):
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep") assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
if not os.path.exists("/tmp/sscep/sscep_dyn"): if not os.path.exists("/tmp/sscep/sscep_dyn"):
@ -1302,6 +1278,7 @@ def test_cli_setup_authority():
assert not os.system("echo '.\n.\n.\n.\nGateway\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem") assert not os.system("echo '.\n.\n.\n.\nGateway\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem") assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem")
# TODO: test e-mails at this point # TODO: test e-mails at this point
# TODO: add strongswan scep client tests here
################### ###################
@ -1314,12 +1291,7 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['expire']) result = runner.invoke(cli, ['expire'])
assert not result.exception, result.output assert not result.exception, result.output
# Shut down server assert os.system("systemctl stop certidude") == 0
assert os.path.exists("/proc/%d" % server_pid)
os.kill(server_pid, 15)
# sleep(2)
# os.kill(server_pid, 9)
os.waitpid(server_pid, 0)
# Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \ assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \