Several updates #4

* Improved offline install docs
* Migrated token mechanism backend to SQL
* Preliminary token mechanism frontend integration
* Add clock skew tolerance for OCSP
* Add 'ldap computer filter' support for Kerberized machine enroll
* Include OCSP and CRL URL-s in certificates, controlled by profile.conf
* Better certificate extension handling
* Place DH parameters file in /etc/ssl/dhparam.pem
* Always talk to CA over port 8443 for 'certidude enroll'
* Hardened frontend nginx config
* Separate log files for frontend nginx
* Better provisioning heuristics
* Add sample site.sh config for LEDE image builder
* Add more device profiles for LEDE image builder
* Various bugfixes and improvements
This commit is contained in:
Lauri Võsandi 2018-05-15 07:45:29 +00:00
parent 728a56a975
commit ce93fbb58b
76 changed files with 1738 additions and 603 deletions

View File

@ -336,26 +336,28 @@ To uninstall:
Offline install Offline install
--------------- ---------------
To set up certificate authority in an isolated environment use a To prepare packages for offline installation use following snippet on a
vanilla Ubuntu 16.04 or container to collect the artifacts: vanilla Ubuntu 16.04 or container:
.. code:: bash .. code:: bash
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
apt install --download-only python3-pip
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
pip3 wheel --wheel-dir=/var/cache/certidude/wheels .
tar -cf certidude-client.tar /var/cache/certidude/wheels
add-apt-repository -y ppa:nginx/stable add-apt-repository -y ppa:nginx/stable
apt-get update -q apt-get update -q
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi
pip3 wheel --wheel-dir=/var/cache/certidude/wheels . tar -cf certidude-server.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
Transfer certidude-artifacts.tar to the target machine and execute: Transfer certidude-server.tar or certidude-client.tar to the target machine and execute:
.. code:: bash .. code:: bash
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
tar -xvf certidude-artifacts.tar -C / tar -xvf certidude-*.tar -C /
dpkg -i /var/cache/apt/archives/*.deb dpkg -i /var/cache/apt/archives/*.deb
pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl

View File

@ -17,6 +17,7 @@ class NormalizeMiddleware(object):
def certidude_app(log_handlers=[]): def certidude_app(log_handlers=[]):
from certidude import authority, config from certidude import authority, config
from certidude.tokens import TokenManager
from .signed import SignedCertificateDetailResource from .signed import SignedCertificateDetailResource
from .request import RequestListResource, RequestDetailResource from .request import RequestListResource, RequestDetailResource
from .lease import LeaseResource, LeaseDetailResource from .lease import LeaseResource, LeaseDetailResource
@ -36,10 +37,20 @@ def certidude_app(log_handlers=[]):
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority))
app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) app.add_route("/api/request/{cn}/", RequestDetailResource(authority))
app.add_route("/api/request/", RequestListResource(authority)) app.add_route("/api/request/", RequestListResource(authority))
app.add_route("/api/", SessionResource(authority))
token_resource = None
token_manager = None
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config
app.add_route("/api/token/", TokenResource(authority)) if config.TOKEN_BACKEND == "sql":
token_manager = TokenManager(config.TOKEN_DATABASE)
token_resource = TokenResource(authority, token_manager)
app.add_route("/api/token/", token_resource)
elif not config.TOKEN_BACKEND:
pass
else:
raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND)
app.add_route("/api/", SessionResource(authority, token_manager))
# Extended attributes for scripting etc. # Extended attributes for scripting etc.
app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))

View File

@ -11,4 +11,5 @@ class LogResource(RelationalMixin):
@authorize_admin @authorize_admin
def on_get(self, req, resp): def on_get(self, req, resp):
# TODO: Add last id parameter # TODO: Add last id parameter
return self.iterfetch("select * from log order by created desc") return self.iterfetch("select * from log order by created desc limit ?",
req.get_param_as_int("limit"))

View File

@ -4,8 +4,8 @@ import os
from asn1crypto.util import timezone from asn1crypto.util import timezone
from asn1crypto import ocsp from asn1crypto import ocsp
from base64 import b64decode from base64 import b64decode
from certidude import config from certidude import config, const
from datetime import datetime from datetime import datetime, timedelta
from oscrypto import asymmetric from oscrypto import asymmetric
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets from .utils.firewall import whitelist_subnets
@ -88,7 +88,8 @@ class OCSPResource(AuthorityHandler):
'serial_number': serial, 'serial_number': serial,
}, },
'cert_status': status, 'cert_status': status,
'this_update': now, 'this_update': now - const.CLOCK_SKEW_TOLERANCE,
'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE,
'single_extensions': [] 'single_extensions': []
}) })

View File

@ -10,6 +10,7 @@ from base64 import b64decode
from certidude import config, push, errors from certidude import config, push, errors
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 certidude.user import DirectoryConnection
from datetime import datetime from datetime import datetime
from oscrypto import asymmetric from oscrypto import asymmetric
from oscrypto.errors import SignatureError from oscrypto.errors import SignatureError
@ -84,13 +85,28 @@ class RequestListResource(AuthorityHandler):
"Bad request", "Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name, machine)) "Common name %s differs from Kerberos credential %s!" % (common_name, machine))
# Automatic enroll with Kerberos machine cerdentials hit = False
resp.set_header("Content-Type", "application/x-pem-file") with DirectoryConnection() as conn:
cert, resp.body = self.authority._sign(csr, body, ft = config.LDAP_COMPUTER_FILTER % ("%s$" % machine)
profile=config.PROFILES["rw"], overwrite=overwrite_allowed) attribs = "cn",
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
machine, req.context.get("remote_addr")) for dn, entry in r:
return if not dn:
continue
else:
hit = True
break
if hit:
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file")
cert, resp.body = self.authority._sign(csr, body,
profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s",
machine, dn, req.context.get("remote_addr"))
return
else:
logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft))
""" """

View File

@ -18,9 +18,13 @@ class CertificateAuthorityResource(object):
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
resp.append_header("Content-Type", "application/x-x509-ca-cert") resp.append_header("Content-Type", "application/x-x509-ca-cert")
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
const.HOSTNAME.encode("ascii")) const.HOSTNAME)
class SessionResource(AuthorityHandler): class SessionResource(AuthorityHandler):
def __init__(self, authority, token_manager):
AuthorityHandler.__init__(self, authority)
self.token_manager = token_manager
@csrf_protection @csrf_protection
@serialize @serialize
@login_required @login_required
@ -97,7 +101,7 @@ class SessionResource(AuthorityHandler):
signer_username = None signer_username = None
# TODO: dedup # TODO: dedup
yield dict( serialized = dict(
serial = "%x" % cert.serial_number, serial = "%x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"), organizational_unit = cert.subject.native.get("organizational_unit_name"),
common_name = common_name, common_name = common_name,
@ -109,12 +113,37 @@ class SessionResource(AuthorityHandler):
lease = lease, lease = lease,
tags = tags, tags = tags,
attributes = attributes or None, attributes = attributes or None,
extensions = dict([ responder_url = None
(e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"]
if e["extn_id"].native in ("extended_key_usage",)])
) )
for e in cert["tbs_certificate"]["extensions"].native:
if e["extn_id"] == "key_usage":
serialized["key_usage"] = e["extn_value"]
elif e["extn_id"] == "extended_key_usage":
serialized["extended_key_usage"] = e["extn_value"]
elif e["extn_id"] == "basic_constraints":
serialized["basic_constraints"] = e["extn_value"]
elif e["extn_id"] == "crl_distribution_points":
for c in e["extn_value"]:
serialized["revoked_url"] = c["distribution_point"]
break
serialized["extended_key_usage"] = e["extn_value"]
elif e["extn_id"] == "authority_information_access":
for a in e["extn_value"]:
if a["access_method"] == "ocsp":
serialized["responder_url"] = a["access_location"]
else:
raise NotImplementedError("Don't know how to handle AIA access method %s" % a["access_method"])
elif e["extn_id"] == "authority_key_identifier":
pass
elif e["extn_id"] == "key_identifier":
pass
elif e["extn_id"] == "subject_alt_name":
serialized["subject_alt_name"], = e["extn_value"]
else:
raise NotImplementedError("Don't know how to handle extension %s" % e["extn_id"])
yield serialized
logger.info("Logged in authority administrator %s from %s with %s" % ( logger.info("Logged in authority administrator %s from %s with %s" % (
req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent"))) req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent")))
return dict( return dict(
@ -130,10 +159,12 @@ class SessionResource(AuthorityHandler):
routers = [j[0] for j in self.authority.list_signed( routers = [j[0] for j in self.authority.list_signed(
common_name=config.SERVICE_ROUTERS)] common_name=config.SERVICE_ROUTERS)]
), ),
builder = dict(
profiles = config.IMAGE_BUILDER_PROFILES or None
),
authority = dict( authority = dict(
builder = dict( hostname = const.FQDN,
profiles = config.IMAGE_BUILDER_PROFILES tokens = self.token_manager.list() if self.token_manager else None,
),
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
lease = dict( lease = dict(
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
@ -145,32 +176,39 @@ class SessionResource(AuthorityHandler):
distinguished_name = cert_to_dn(self.authority.certificate), distinguished_name = cert_to_dn(self.authority.certificate),
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
blob = self.authority.certificate_buf.decode("ascii"), blob = self.authority.certificate_buf.decode("ascii"),
organization = self.authority.certificate["tbs_certificate"]["subject"].native.get("organization_name"),
signed = self.authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None),
expires = self.authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
), ),
mailer = dict( mailer = dict(
name = config.MAILER_NAME, name = config.MAILER_NAME,
address = config.MAILER_ADDRESS address = config.MAILER_ADDRESS
) if config.MAILER_ADDRESS else None, ) if config.MAILER_ADDRESS else None,
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
requests=serialize_requests(self.authority.list_requests), requests=serialize_requests(self.authority.list_requests),
signed=serialize_certificates(self.authority.list_signed), signed=serialize_certificates(self.authority.list_signed),
revoked=serialize_revoked(self.authority.list_revoked), revoked=serialize_revoked(self.authority.list_revoked),
admin_users = User.objects.filter_admins(),
user_subnets = config.USER_SUBNETS or None,
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
request_subnets = config.REQUEST_SUBNETS or None,
admin_subnets=config.ADMIN_SUBNETS or None,
signature = dict( signature = dict(
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
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")),
) )
), ),
authorization = dict(
admin_users = User.objects.filter_admins(),
user_subnets = config.USER_SUBNETS or None,
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
request_subnets = config.REQUEST_SUBNETS or None,
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS or None,
admin_subnets=config.ADMIN_SUBNETS or None,
ocsp_subnets = config.OCSP_SUBNETS or None,
crl_subnets = config.CRL_SUBNETS or None,
scep_subnets = config.SCEP_SUBNETS or None,
),
features=dict( features=dict(
ocsp=bool(config.OCSP_SUBNETS),
crl=bool(config.CRL_SUBNETS),
token=bool(config.TOKEN_URL), token=bool(config.TOKEN_URL),
tagging=True, tagging=True,
leases=True, leases=True,

View File

@ -1,11 +1,16 @@
import click
import codecs
import falcon import falcon
import logging import logging
import hashlib import os
import string
from asn1crypto import pem from asn1crypto import pem
from asn1crypto.csr import CertificationRequest from asn1crypto.csr import CertificationRequest
from datetime import datetime from datetime import datetime, timedelta
from time import time from time import time
from certidude import mailer from certidude import mailer, const
from certidude.tokens import TokenManager
from certidude.relational import RelationalMixin
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
@ -15,33 +20,25 @@ from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TokenResource(AuthorityHandler): class TokenResource(AuthorityHandler):
def __init__(self, authority, manager):
AuthorityHandler.__init__(self, authority)
self.manager = manager
def on_get(self, req, resp):
return
def on_put(self, req, resp): def on_put(self, req, resp):
# Consume token try:
now = time() username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True))
timestamp = req.get_param_as_int("t", required=True) except RelationalMixin.DoesNotExist:
username = req.get_param("u", required=True) raise falcon.HTTPForbidden("Forbidden", "No such token or token expired")
user = User.objects.get(username)
csum = hashlib.sha256()
csum.update(config.TOKEN_SECRET)
csum.update(username.encode("ascii"))
csum.update(str(timestamp).encode("ascii"))
margin = 300 # Tolerate 5 minute clock skew as Kerberos does
if csum.hexdigest() != req.get_param("c", required=True):
raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?")
if now < timestamp - margin:
raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?")
if now > timestamp + margin + config.TOKEN_LIFETIME:
raise falcon.HTTPForbidden("Forbidden", "Token expired")
# At this point consider token to be legitimate
body = req.stream.read(req.content_length) body = req.stream.read(req.content_length)
header, _, der_bytes = pem.unarmor(body) header, _, der_bytes = pem.unarmor(body)
csr = CertificationRequest.load(der_bytes) csr = CertificationRequest.load(der_bytes)
common_name = csr["certification_request_info"]["subject"].native["common_name"] common_name = csr["certification_request_info"]["subject"].native["common_name"]
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
try: try:
_, resp.body = self.authority._sign(csr, body, profile="default", _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile),
overwrite=config.TOKEN_OVERWRITE_PERMITTED) overwrite=config.TOKEN_OVERWRITE_PERMITTED)
resp.set_header("Content-Type", "application/x-pem-file") resp.set_header("Content-Type", "application/x-pem-file")
logger.info("Autosigned %s as proven by token ownership", common_name) logger.info("Autosigned %s as proven by token ownership", common_name)
@ -56,40 +53,7 @@ class TokenResource(AuthorityHandler):
@login_required @login_required
@authorize_admin @authorize_admin
def on_post(self, req, resp): def on_post(self, req, resp):
# Generate token self.manager.issue(
issuer = req.context.get("user") issuer = req.context.get("user"),
username = req.get_param("username") subject = User.objects.get(req.get_param("username", required=True)),
secondary = req.get_param("mail") subject_mail = req.get_param("mail"))
if username:
# Otherwise try to look up user so we can derive their e-mail address
user = User.objects.get(username)
else:
# If no username is specified, assume it's intended for someone outside domain
username = "guest-%s" % hashlib.sha256(secondary.encode("ascii")).hexdigest()[-8:]
if not secondary:
raise
timestamp = int(time())
csum = hashlib.sha256()
csum.update(config.TOKEN_SECRET)
csum.update(username.encode("ascii"))
csum.update(str(timestamp).encode("ascii"))
args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name)
# Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata
token_created = datetime.fromtimestamp(timestamp)
token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME)
try:
with open("/etc/timezone") as fh:
token_timezone = fh.read().strip()
except EnvironmentError:
token_timezone = None
url = "%s#%s" % (config.TOKEN_URL, args)
context = globals()
context.update(locals())
mailer.send("token.md", to=user, **context)
return {
"token": args,
"url": url,
}

View File

@ -110,7 +110,7 @@ def authenticate(optional=False):
if kerberized: if kerberized:
if not req.auth.startswith("Negotiate "): if not req.auth.startswith("Negotiate "):
raise falcon.HTTPBadRequest("Bad request", raise falcon.HTTPBadRequest("Bad request",
"Bad header, expected Negotiate: %s" % req.auth) "Bad header, expected Negotiate")
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
@ -158,7 +158,7 @@ def authenticate(optional=False):
else: else:
if not req.auth.startswith("Basic "): if not req.auth.startswith("Basic "):
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth) raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic")
basic, token = req.auth.split(" ", 1) basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1) user, passwd = b64decode(token).decode("ascii").split(":", 1)

View File

@ -13,26 +13,14 @@ from asn1crypto.csr import CertificationRequest
from certbuilder import CertificateBuilder from certbuilder import CertificateBuilder
from certidude import config, push, mailer, const from certidude import config, push, mailer, const
from certidude import errors from certidude import errors
from certidude.common import cn_to_dn from certidude.common import cn_to_dn, generate_serial, random
from crlbuilder import CertificateListBuilder, pem_armor_crl from crlbuilder import CertificateListBuilder, pem_armor_crl
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from datetime import datetime, timedelta from datetime import datetime, timedelta
from jinja2 import Template from jinja2 import Template
from random import SystemRandom
from xattr import getxattr, listxattr, setxattr from xattr import getxattr, listxattr, setxattr
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
random = SystemRandom()
try:
from time import time_ns
except ImportError:
from time import time
def time_ns():
return int(time() * 10**9) # 64 bits integer, 32 ns bits
def generate_serial():
return time_ns() << 56 | random.randint(0, 2**56-1)
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
# https://jamielinux.com/docs/openssl-certificate-authority/ # https://jamielinux.com/docs/openssl-certificate-authority/
@ -61,13 +49,14 @@ def self_enroll(skip_notify=False):
self_public_key = asymmetric.load_public_key(path) self_public_key = asymmetric.load_public_key(path)
private_key = asymmetric.load_private_key(config.SELF_KEY_PATH) private_key = asymmetric.load_private_key(config.SELF_KEY_PATH)
except FileNotFoundError: # certificate or private key not found except FileNotFoundError: # certificate or private key not found
click.echo("Generating private key for frontend: %s" % config.SELF_KEY_PATH)
with open(config.SELF_KEY_PATH, 'wb') as fh: with open(config.SELF_KEY_PATH, 'wb') as fh:
if public_key.algorithm == "ec": if public_key.algorithm == "ec":
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
elif public_key.algorithm == "rsa": elif public_key.algorithm == "rsa":
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size) self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
else: else:
NotImplemented raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm)
fh.write(asymmetric.dump_private_key(private_key, None)) fh.write(asymmetric.dump_private_key(private_key, None))
else: else:
now = datetime.utcnow() now = datetime.utcnow()
@ -84,10 +73,11 @@ def self_enroll(skip_notify=False):
drop_privileges() drop_privileges()
assert os.getuid() != 0 and os.getgid() != 0 assert os.getuid() != 0 and os.getgid() != 0
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
click.echo("Writing request to %s" % path) click.echo("Writing certificate signing request for frontend: %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_notify=skip_notify, 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"])
click.echo("Frontend certificate signed")
sys.exit(0) sys.exit(0)
else: else:
os.waitpid(pid, 0) os.waitpid(pid, 0)
@ -409,13 +399,15 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
builder.serial_number = generate_serial() builder.serial_number = generate_serial()
now = datetime.utcnow() now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5) builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE
builder.end_date = now + timedelta(days=profile.lifetime) builder.end_date = now + timedelta(days=profile.lifetime)
builder.issuer = certificate builder.issuer = certificate
builder.ca = profile.ca builder.ca = profile.ca
builder.key_usage = profile.key_usage builder.key_usage = profile.key_usage
builder.extended_key_usage = profile.extended_key_usage builder.extended_key_usage = profile.extended_key_usage
builder.subject_alt_domains = [common_name] builder.subject_alt_domains = [common_name]
builder.ocsp_url = profile.responder_url
builder.crl_url = profile.revoked_url
end_entity_cert = builder.build(private_key) end_entity_cert = builder.build(private_key)
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)

View File

@ -18,7 +18,7 @@ from certbuilder import CertificateBuilder, pem_armor_certificate
from certidude import const from certidude import const
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from configparser import ConfigParser, NoOptionError from configparser import ConfigParser, NoOptionError
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial
from datetime import datetime, timedelta from datetime import datetime, timedelta
from glob import glob from glob import glob
from ipaddress import ip_network from ipaddress import ip_network
@ -51,7 +51,7 @@ def setup_client(prefix="client_", dh=False):
authority = arguments.get("authority") authority = arguments.get("authority")
b = os.path.join("/etc/certidude/authority", authority) b = os.path.join("/etc/certidude/authority", authority)
if dh: if dh:
path = os.path.join(const.STORAGE_PATH, "dh.pem") path = os.path.join("/etc/ssl/dhparam.pem")
if not os.path.exists(path): if not os.path.exists(path):
rpm("openssl") rpm("openssl")
apt("openssl") apt("openssl")
@ -390,7 +390,6 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
} }
if renew: # Do mutually authenticated TLS handshake if renew: # Do mutually authenticated TLS handshake
request_url = "https://%s:8443/api/request/" % authority_name
kwargs["cert"] = certificate_path, key_path kwargs["cert"] = certificate_path, key_path
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"]) click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
else: else:
@ -417,8 +416,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
else: else:
click.echo("Not using machine keytab") click.echo("Not using machine keytab")
request_url = "https://%s/api/request/" % authority_name
request_url = "https://%s:8443/api/request/" % authority_name
if request_params: if request_params:
request_url = request_url + "?" + "&".join(request_params) request_url = request_url + "?" + "&".join(request_params)
submission = requests.post(request_url, **kwargs) submission = requests.post(request_url, **kwargs)
@ -580,7 +579,7 @@ 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" % ( nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % (
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA")) "ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
nm_config.set("vpn", "cipher", "AES-128-GCM") nm_config.set("vpn", "cipher", "AES-128-GCM")
nm_config.set("vpn", "auth", "SHA384") nm_config.set("vpn", "auth", "SHA384")
@ -995,6 +994,10 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
default="/etc/nginx/sites-available/certidude.conf", default="/etc/nginx/sites-available/certidude.conf",
type=click.File(mode="w", atomic=True, lazy=True), type=click.File(mode="w", atomic=True, lazy=True),
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default") help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
@click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN) @click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN) @click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
@ -1008,7 +1011,7 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@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")
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA") @click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
@fqdn_required @fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate): def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
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 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"
@ -1027,21 +1030,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
rsync attr wget unzip" rsync attr wget unzip"
click.echo("Running: %s" % cmd) click.echo("Running: %s" % cmd)
if os.system(cmd): sys.exit(254) if os.system(cmd):
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253) raise click.ClickException("Failed to install APT packages")
if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252) if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"):
raise click.ClickException("Failed to install Python packages")
if os.system("pip3 install -q --pre --upgrade python-ldap"):
raise click.ClickException("Failed to install python-ldap")
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
click.echo("Enabling nginx PPA") click.echo("Enabling nginx PPA")
if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251) if os.system("add-apt-repository -y ppa:nginx/stable"):
if os.system("apt-get update -q"): sys.exit(250) raise click.ClickException("Failed to add nginx PPA")
if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249) if os.system("apt-get update -q"):
raise click.ClickException("Failed to update package lists")
if os.system("apt-get install -y -q libnginx-mod-nchan"):
raise click.ClickException("Failed to install nchan")
else: else:
click.echo("PPA for nginx already enabled") click.echo("PPA for nginx already enabled")
if not os.path.exists("/usr/sbin/nginx"): if not os.path.exists("/usr/sbin/nginx"):
click.echo("Installing nginx from PPA") click.echo("Installing nginx from PPA")
if os.system("apt-get install -y -q nginx"): sys.exit(248) if os.system("apt-get install -y -q nginx"):
raise click.ClickException("Failed to install nginx")
else: else:
click.echo("Web server nginx already installed") click.echo("Web server nginx already installed")
@ -1049,7 +1059,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
os.symlink("/usr/bin/nodejs", "/usr/bin/node") os.symlink("/usr/bin/nodejs", "/usr/bin/node")
# Generate secret for tokens # Generate secret for tokens
token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50)) token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn"
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
click.echo("Using templates from %s" % template_path) click.echo("Using templates from %s" % template_path)
@ -1062,6 +1072,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
revoked_url = "http://%s/api/revoked/" % common_name revoked_url = "http://%s/api/revoked/" % common_name
click.echo("Setting revocation list URL to %s" % revoked_url) click.echo("Setting revocation list URL to %s" % revoked_url)
responder_url = "http://%s/api/ocsp/" % common_name
click.echo("Setting OCSP responder URL to %s" % responder_url)
# Expand variables # Expand variables
assets_dir = os.path.join(directory, "assets") assets_dir = os.path.join(directory, "assets")
ca_key = os.path.join(directory, "ca_key.pem") ca_key = os.path.join(directory, "ca_key.pem")
@ -1070,6 +1084,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
self_key = os.path.join(directory, "self_key.pem") self_key = os.path.join(directory, "self_key.pem")
sqlite_path = os.path.join(directory, "meta", "db.sqlite") sqlite_path = os.path.join(directory, "meta", "db.sqlite")
distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit) distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit)
dhparam_path = "/etc/ssl/dhparam.pem"
# Builder variables # Builder variables
dhgroup = "ecp384" if elliptic_curve else "modp2048" dhgroup = "ecp384" if elliptic_curve else "modp2048"
@ -1080,8 +1095,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
except KeyError: except KeyError:
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
if subprocess.call(cmd): if subprocess.call(cmd):
click.echo("Failed to create system user 'certidude'") raise click.ClickException("Failed to create system user 'certidude'")
return 255
if os.path.exists(kerberos_keytab): if os.path.exists(kerberos_keytab):
click.echo("Service principal keytab found in '%s'" % kerberos_keytab) click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
@ -1114,6 +1128,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
letsencrypt = os.path.exists(letsencrypt_fullchain) 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")
@ -1163,26 +1178,27 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
if skip_packages: if skip_packages:
click.echo("Not attempting to install packages from NPM as requested...") click.echo("Not attempting to install packages from NPM as requested...")
else: else:
cmd = "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" cmd = "npm install --silent --no-optional -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"
click.echo("Installing JavaScript packages: %s" % cmd) click.echo("Installing JavaScript packages: %s" % cmd)
if os.system(cmd): sys.exit(230)
if skip_assets: if skip_assets:
click.echo("Not attempting to assemble assets as requested...") click.echo("Not attempting to assemble assets as requested...")
else: else:
# Copy fonts # Copy fonts
click.echo("Copying fonts...") click.echo("Copying fonts...")
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229) if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir):
raise click.ClickException("Failed to copy fonts")
# Compile nunjucks templates # Compile nunjucks templates
cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js)
click.echo("Compiling templates: %s" % cmd) click.echo("Compiling templates: %s" % cmd)
if os.system(cmd): sys.exit(228) if os.system(cmd):
raise click.ClickException("Failed to compile nunjucks templates")
# Assemble bundle.js # Assemble bundle.js
click.echo("Assembling %s" % bundle_js) click.echo("Assembling %s" % bundle_js)
with open(bundle_js + ".part", "a") as fh: with open(bundle_js + ".part", "a") as fh:
for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js": for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js":
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
click.echo("- Merging: %s" % j) click.echo("- Merging: %s" % j)
with open(j) as ih: with open(j) as ih:
@ -1208,9 +1224,22 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
if not os.path.exists(const.CONFIG_DIR): if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % const.CONFIG_DIR) click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR) os.makedirs(const.CONFIG_DIR)
if not os.path.exists(const.SCRIPT_DIR):
click.echo("Creating %s" % const.SCRIPT_DIR)
os.makedirs(const.SCRIPT_DIR)
os.umask(0o177) # 600 os.umask(0o177) # 600
if not os.path.exists(dhparam_path):
cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE))
subprocess.check_call(cmd)
if os.path.exists(tls_config.name):
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
else:
tls_config.write(env.get_template("nginx-tls.conf").render(locals()))
click.echo("Generated %s" % tls_config.name)
if os.path.exists(const.SERVER_CONFIG_PATH): if os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
else: else:
@ -1227,6 +1256,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
fh.write(env.get_template("server/builder.conf").render(vars())) fh.write(env.get_template("server/builder.conf").render(vars()))
click.echo("File %s created" % const.BUILDER_CONFIG_PATH) click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
# Create image builder site script
if os.path.exists(const.BUILDER_SITE_SCRIPT):
click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT)
else:
with open(const.BUILDER_SITE_SCRIPT, "w") as fh:
fh.write(env.get_template("server/site.sh").render(vars()))
click.echo("File %s created" % const.BUILDER_SITE_SCRIPT)
# Create signature profile config # Create signature profile config
if os.path.exists(const.PROFILE_CONFIG_PATH): if os.path.exists(const.PROFILE_CONFIG_PATH):
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
@ -1235,20 +1272,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
fh.write(env.get_template("server/profile.conf").render(vars())) fh.write(env.get_template("server/profile.conf").render(vars()))
click.echo("File %s created" % const.PROFILE_CONFIG_PATH) click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
if not os.path.exists("/var/lib/certidude/builder"):
click.echo("Creating %s" % "/var/lib/certidude/builder")
os.makedirs("/var/lib/certidude/builder")
# Create subdirectories with 770 permissions # Create subdirectories with 770 permissions
os.umask(0o007) os.umask(0o007)
for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"): for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"):
path = os.path.join(directory, subdir) path = os.path.join(directory, subdir)
if not os.path.exists(path): if not os.path.exists(path):
click.echo("Creating directory %s" % path) click.echo("Creating directory %s" % path)
os.mkdir(path) os.mkdir(path)
else: else:
click.echo("Directory already exists %s" % path) click.echo("Directory already exists %s" % path)
assert os.stat(path).st_mode == 0o40770 assert os.stat(path).st_mode == 0o40770, path
# Create SQLite database file with correct permissions # Create SQLite database file with correct permissions
os.umask(0o117) os.umask(0o117)
@ -1293,17 +1326,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
click.echo(" chmod 0644 %s" % ca_cert) click.echo(" chmod 0644 %s" % ca_cert)
click.echo() click.echo()
click.echo("To finish setup procedure run 'certidude setup authority' again") click.echo("To finish setup procedure run 'certidude setup authority' again")
sys.exit(1) sys.exit(1) # stop this fork here with error
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
builder = CertificateBuilder(distinguished_name, public_key) builder = CertificateBuilder(distinguished_name, public_key)
builder.self_signed = True builder.self_signed = True
builder.ca = True builder.ca = True
builder.serial_number = random.randint( builder.serial_number = generate_serial()
0x100000000000000000000000000000000000000,
0xfffffffffffffffffffffffffffffffffffffff)
builder.begin_date = NOW - timedelta(minutes=5) builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE
builder.end_date = NOW + timedelta(days=authority_lifetime) builder.end_date = NOW + timedelta(days=authority_lifetime)
certificate = builder.build(private_key) certificate = builder.build(private_key)
@ -1312,14 +1343,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
os.umask(0o137) os.umask(0o137)
with open(ca_cert, 'wb') as f: with open(ca_cert, 'wb') as f:
f.write(pem_armor_certificate(certificate)) f.write(pem_armor_certificate(certificate))
click.echo("Authority certificate written to: %s" % ca_cert)
sys.exit(0) # stop this fork here sys.exit(0) # stop this fork here
else: else:
_, exitcode = os.waitpid(bootstrap_pid, 0) _, exitcode = os.waitpid(bootstrap_pid, 0)
if exitcode: if exitcode:
return 0 return 0
from certidude import authority from certidude import authority
authority.self_enroll(skip_notify=True) authority.self_enroll(skip_notify=True)
assert os.path.exists(self_key)
assert os.path.exists(os.path.join(directory, "signed", const.FQDN) + ".pem")
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
assert os.stat(sqlite_path).st_mode == 0o100660 assert os.stat(sqlite_path).st_mode == 0o100660
assert os.stat(ca_cert).st_mode == 0o100640 assert os.stat(ca_cert).st_mode == 0o100640
@ -1343,6 +1378,11 @@ 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 inspect logs and issued tokens:")
click.echo()
click.echo(" echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
click.echo(" echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
return 0 return 0
@ -1461,7 +1501,7 @@ def certidude_revoke(common_name, reason):
@click.command("expire", help="Move expired certificates") @click.command("expire", help="Move expired certificates")
def certidude_expire(): def certidude_expire():
from certidude import authority, config from certidude import authority, config
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE
for common_name, path, buf, cert, signed, expires in authority.list_signed(): for common_name, path, buf, cert, signed, expires in authority.list_signed():
if expires < threshold: if expires < threshold:
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number) expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
@ -1492,6 +1532,10 @@ def certidude_serve(port, listen, fork):
from certidude import config from certidude import config
click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS)
click.echo("CRL subnets: %s" % config.CRL_SUBNETS)
click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS)
click.echo("Loading signature profiles:") click.echo("Loading signature profiles:")
for profile in config.PROFILES.values(): for profile in config.PROFILES.values():
click.echo("- %s" % profile) click.echo("- %s" % profile)
@ -1625,6 +1669,35 @@ def certidude_test(recipient):
to=recipient to=recipient
) )
@click.command("list", help="List tokens")
def certidude_token_list():
from certidude import config
from certidude.tokens import TokenManager
token_manager = TokenManager(config.TOKEN_DATABASE)
cols = "uuid", "expires", "subject", "state"
now = datetime.utcnow()
for token in token_manager.list(expired=True, used=True, token=True):
token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now else "expired")
print(";".join([str(token.get(col)) for col in cols]))
@click.command("purge", help="Purge tokens")
@click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens")
def certidude_token_purge(all):
from certidude import config
from certidude.tokens import TokenManager
token_manager = TokenManager(config.TOKEN_DATABASE)
print(token_manager.purge(all))
@click.command("issue", help="Issue token")
@click.option("-m", "--subject-mail", default=None, help="Subject e-mail override")
@click.argument("subject")
def certidude_token_issue(subject, subject_mail):
from certidude import config
from certidude.tokens import TokenManager
from certidude.user import User
token_manager = TokenManager(config.TOKEN_DATABASE)
token_manager.issue(None, User.objects.get(subject), subject_mail)
@click.group("strongswan", help="strongSwan helpers") @click.group("strongswan", help="strongSwan helpers")
def certidude_setup_strongswan(): pass def certidude_setup_strongswan(): pass
@ -1635,6 +1708,9 @@ def certidude_setup_openvpn(): pass
@click.group("setup", help="Getting started section") @click.group("setup", help="Getting started section")
def certidude_setup(): pass def certidude_setup(): pass
@click.group("token", help="Token management")
def certidude_token(): pass
@click.group() @click.group()
def entry_point(): pass def entry_point(): pass
@ -1649,6 +1725,10 @@ certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_strongswan)
certidude_setup.add_command(certidude_setup_nginx) certidude_setup.add_command(certidude_setup_nginx)
certidude_setup.add_command(certidude_setup_yubikey) certidude_setup.add_command(certidude_setup_yubikey)
certidude_token.add_command(certidude_token_list)
certidude_token.add_command(certidude_token_purge)
certidude_token.add_command(certidude_token_issue)
entry_point.add_command(certidude_token)
entry_point.add_command(certidude_setup) entry_point.add_command(certidude_setup)
entry_point.add_command(certidude_serve) entry_point.add_command(certidude_serve)
entry_point.add_command(certidude_enroll) entry_point.add_command(certidude_enroll)

View File

@ -2,6 +2,16 @@
import os import os
import click import click
import subprocess import subprocess
from random import SystemRandom
random = SystemRandom()
try:
from time import time_ns
except ImportError:
from time import time
def time_ns():
return int(time() * 10**9) # 64 bits integer, 32 ns bits
MAPPING = dict( MAPPING = dict(
common_name="CN", common_name="CN",
@ -122,3 +132,6 @@ def pip(packages):
pip.main(['install'] + packages.split(" ")) pip.main(['install'] + packages.split(" "))
return True return True
def generate_serial():
return time_ns() << 56 | random.randint(0, 2**56-1)

View File

@ -23,6 +23,7 @@ LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
LDAP_BASE = cp.get("accounts", "ldap base") LDAP_BASE = cp.get("accounts", "ldap base")
LDAP_MAIL_ATTRIBUTE = cp.get("accounts", "ldap mail attribute")
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])
@ -71,8 +72,7 @@ USER_MULTIPLE_CERTIFICATES = {
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url")
AUTHORITY_CRL_URL = cp.get("signature", "revoked url") AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN
AUTHORITY_OCSP_URL = cp.get("signature", "responder url")
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
@ -88,6 +88,8 @@ USERS_GROUP = cp.get("authorization", "posix user group")
ADMIN_GROUP = cp.get("authorization", "posix admin group") ADMIN_GROUP = cp.get("authorization", "posix admin group")
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter")
LDAP_COMPUTER_FILTER = cp.get("authorization", "ldap computer filter")
if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'")
if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'")
@ -95,9 +97,9 @@ TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("taggi
# Tokens # Tokens
TOKEN_URL = cp.get("token", "url") TOKEN_URL = cp.get("token", "url")
TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to seconds TOKEN_BACKEND = cp.get("token", "backend")
TOKEN_SECRET = cp.get("token", "secret").encode("ascii") TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds
TOKEN_DATABASE = cp.get("token", "database")
# TODO: Check if we don't have base or servers # TODO: Check if we don't have base or servers
# The API call for looking up scripts uses following directory as root # The API call for looking up scripts uses following directory as root
@ -115,11 +117,13 @@ PROFILES = dict([(key, SignatureProfile(key,
profile_config.get(key, "key usage"), profile_config.get(key, "key usage"),
profile_config.get(key, "extended key usage"), profile_config.get(key, "extended key usage"),
profile_config.get(key, "common name"), profile_config.get(key, "common name"),
)) for key in profile_config.sections()]) profile_config.get(key, "revoked url"),
profile_config.get(key, "responder url")
)) for key in profile_config.sections() if profile_config.getboolean(key, "enabled")])
cp2 = configparser.RawConfigParser() cp2 = configparser.RawConfigParser()
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")]
TOKEN_OVERWRITE_PERMITTED=True TOKEN_OVERWRITE_PERMITTED=True

View File

@ -3,17 +3,21 @@ import click
import os import os
import socket import socket
import sys import sys
from datetime import timedelta
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
CURVE_NAME = "secp384r1" CURVE_NAME = "secp384r1"
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance
RUN_DIR = "/run/certidude" RUN_DIR = "/run/certidude"
CONFIG_DIR = "/etc/certidude" CONFIG_DIR = "/etc/certidude"
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
SCRIPT_DIR = os.path.join(CONFIG_DIR, "script")
BUILDER_SITE_SCRIPT = os.path.join(SCRIPT_DIR, "site.sh")
PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")

View File

@ -4,7 +4,7 @@ from datetime import timedelta
from certidude import const from certidude import const
class SignatureProfile(object): class SignatureProfile(object):
def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name): def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name, revoked_url, responder_url):
self.slug = slug self.slug = slug
self.title = title self.title = title
self.ou = ou or None self.ou = ou or None
@ -12,6 +12,9 @@ class SignatureProfile(object):
self.lifetime = lifetime self.lifetime = lifetime
self.key_usage = set(key_usage.split(" ")) if key_usage else set() self.key_usage = set(key_usage.split(" ")) if key_usage else set()
self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set()
self.responder_url = responder_url
self.revoked_url = revoked_url
if common_name.startswith("^"): if common_name.startswith("^"):
self.common_name = common_name self.common_name = common_name
elif common_name == "RE_HOSTNAME": elif common_name == "RE_HOSTNAME":
@ -39,7 +42,7 @@ class SignatureProfile(object):
def serialize(self): def serialize(self):
return dict([(key, getattr(self,key)) for key in ( return dict([(key, getattr(self,key)) for key in (
"slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")]) "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name", "responder_url", "revoked_url")])
def __repr__(self): def __repr__(self):
bits = [] bits = []
@ -47,6 +50,10 @@ class SignatureProfile(object):
bits.append("%d years" % (self.lifetime / 365)) bits.append("%d years" % (self.lifetime / 365))
if self.lifetime % 365: if self.lifetime % 365:
bits.append("%d days" % (self.lifetime % 365)) bits.append("%d days" % (self.lifetime % 365))
return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % ( return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s, responder_url=%s, revoked_url=%s)" % (
self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name) self.slug, self.title, self.ca, self.ou, " ".join(bits),
self.key_usage, self.extended_key_usage,
repr(self.common_name),
repr(self.responder_url),
repr(self.revoked_url))

View File

@ -14,6 +14,9 @@ class RelationalMixin(object):
SQL_CREATE_TABLES = "" SQL_CREATE_TABLES = ""
class DoesNotExist(Exception):
pass
def __init__(self, uri): def __init__(self, uri):
self.uri = urlparse(uri) self.uri = urlparse(uri)
@ -29,7 +32,8 @@ class RelationalMixin(object):
if self.uri.netloc: if self.uri.netloc:
raise ValueError("Malformed database URI %s" % self.uri) raise ValueError("Malformed database URI %s" % self.uri)
import sqlite3 import sqlite3
conn = sqlite3.connect(self.uri.path) conn = sqlite3.connect(self.uri.path,
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
else: else:
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme)
@ -74,7 +78,6 @@ class RelationalMixin(object):
conn.close() conn.close()
return rowid return rowid
def iterfetch(self, query, *args): def iterfetch(self, query, *args):
conn = self.sql_connect() conn = self.sql_connect()
cursor = conn.cursor() cursor = conn.cursor()
@ -86,3 +89,24 @@ class RelationalMixin(object):
cursor.close() cursor.close()
conn.close() conn.close()
return tuple(g()) return tuple(g())
def get(self, query, *args):
conn = self.sql_connect()
cursor = conn.cursor()
cursor.execute(query, args)
row = cursor.fetchone()
cursor.close()
conn.close()
if not row:
raise self.DoesNotExist("No matches for query '%s' with parameters %s" % (query, repr(args)))
return row
def execute(self, query, *args):
conn = self.sql_connect()
cursor = conn.cursor()
cursor.execute(query, args)
affected_rows = cursor.rowcount
cursor.close()
conn.commit()
conn.close()
return affected_rows

View File

@ -1,4 +1,5 @@
create table if not exists log ( create table if not exists log (
id integer primary key autoincrement,
created datetime, created datetime,
facility varchar(30), facility varchar(30),
level int, level int,

View File

@ -0,0 +1,17 @@
insert into token (
created,
expires,
uuid,
issuer,
subject,
mail,
profile
) values (
?,
?,
?,
?,
?,
?,
?
);

View File

@ -0,0 +1,13 @@
create table if not exists token (
id integer primary key autoincrement,
created datetime,
used datetime,
expires datetime,
uuid char(32),
issuer char(30),
subject varchar(30),
mail varchar(128),
profile varchar(10),
constraint unique_uuid unique(uuid)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

View File

@ -20,20 +20,14 @@
<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"> <li class="nav-item">
<a class="nav-link disabled dashboard" href="#columns=2">Dashboard</a> <a class="nav-link disabled dashboard" href="#">Dashboard</a>
</li> </li>
<li class="nav-item"> <li class="nav-item hidden-xl-up">
<a class="nav-link" href="#columns=3">Wider</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#columns=4">Widest</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Log</a> <a class="nav-link" 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 id="search" class="form-control mr-sm-2" type="search" placeholder="🔍"> <input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍">
</form> </form>
</div> </div>
</nav> </nav>

View File

@ -1,15 +1,8 @@
'use strict'; 'use strict';
const KEYWORDS = [ const KEY_SIZE = 2048;
["Android", "android"], const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"];
["iPhone", "iphone"],
["iPad", "ipad"],
["Ubuntu", "ubuntu"],
["Fedora", "fedora"],
["Linux", "linux"],
["Macintosh", "mac"],
];
jQuery.timeago.settings.allowFuture = true; jQuery.timeago.settings.allowFuture = true;
@ -17,38 +10,217 @@ function normalizeCommonName(j) {
return j.replace("@", "--").split(".").join("-"); // dafuq ?! return j.replace("@", "--").split(".").join("-"); // dafuq ?!
} }
function onShowAll() {
var options = document.querySelectorAll(".option");
for (i = 0; i < options.length; i++) {
options[i].style.display = "block";
}
}
function onKeyGen() {
if (window.navigator.userAgent.indexOf(" Edge/") >= 0) {
$("#enroll .loader-container").hide();
$("#enroll .edge-broken").show();
return;
}
window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE);
console.info('Key-pair created.');
// Device identifier
var dig = forge.md.sha384.create();
dig.update(window.navigator.userAgent);
var prefix = "unknown";
for (i in DEVICE_KEYWORDS) {
var keyword = DEVICE_KEYWORDS[i];
if (window.navigator.userAgent.indexOf(keyword) >= 0) {
prefix = keyword.toLowerCase();
break;
}
}
window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8);
console.info("Device identifier:", identifier);
window.common_name = query.subject + "@" + identifier;
window.csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey;
csr.setSubject([{
name: 'commonName', value: common_name
}]);
csr.sign(keys.privateKey, forge.md.sha384.create());
console.info('Certification request created');
$("#enroll .loader-container").hide();
var prefix = null;
for (i in DEVICE_KEYWORDS) {
var keyword = DEVICE_KEYWORDS[i];
if (window.navigator.userAgent.indexOf(keyword) >= 0) {
prefix = keyword.toLowerCase();
break;
}
}
if (prefix == null) {
$(".option").show();
return;
}
var protocols = query.protocols.split(",");
console.info("Showing snippets for:", protocols);
for (var j = 0; j < protocols.length; j++) {
var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix);
for (i = 0; i < options.length; i++) {
options[i].style.display = "block";
}
}
}
function onEnroll(encoding) {
console.info("User agent:", window.navigator.userAgent);
var xhr = new XMLHttpRequest();
xhr.open('GET', "/api/certificate");
xhr.onload = function() {
if (xhr.status === 200) {
var ca = forge.pki.certificateFromPem(xhr.responseText);
console.info("Got CA certificate:");
var xhr2 = new XMLHttpRequest();
xhr2.open("PUT", "/api/token/?token=" + query.token );
xhr2.onload = function() {
if (xhr2.status === 200) {
var a = document.createElement("a");
var cert = forge.pki.certificateFromPem(xhr2.responseText);
console.info("Got signed certificate:", xhr2.responseText);
var p12 = forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "", {algorithm: '3des'});
switch(encoding) {
case 'p12':
var buf = forge.asn1.toDer(p12).getBytes();
var mimetype = "application/x-pkcs12"
a.download = query.router + ".p12";
break
case 'sswan':
var buf = JSON.stringify({
uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618",
name: identifier,
type: "ikev2-cert",
'ike-proposal': 'aes256-sha384-prfsha384-modp2048',
'esp-proposal': 'aes128gcm16-aes128gmac-modp2048',
remote: { addr: query.router },
local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) }
});
console.info("Buf is:", buf);
var mimetype = "application/vnd.strongswan.profile"
a.download = query.router + ".sswan";
break
case 'ovpn':
var buf = nunjucks.render('snippets/openvpn-client.conf', {
session: {
authority: {
certificate: {
common_name: "Certidude at " + window.location.hostname,
algorithm: "rsa"
}
},
service: {
protocols: query.protocols.split(","),
routers: [query.router],
}
},
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: xhr2.responseText,
ca: xhr.responseText
});
var mimetype = "application/x-openvpn-profile";
a.download = query.router + ".ovpn";
break
case 'mobileconfig':
var p12 = forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'});
var buf = nunjucks.render('snippets/ios.mobileconfig', {
session: {
authority: {
certificate: {
common_name: "Certidude at " + window.location.hostname,
algorithm: "rsa"
}
}
},
common_name: common_name,
gateway: query.router,
p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()),
ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes())
});
var mimetype = "application/x-apple-aspen-config";
a.download = query.router + ".mobileconfig";
break
}
a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf);
console.info("Offering bundle for download");
document.body.appendChild(a); // Firefox needs this!
a.click();
} else {
if (xhr2.status == 403) { alert("Token used or expired"); }
console.info('Request failed. Returned status of ' + xhr2.status);
try {
var r = JSON.parse(xhr2.responseText);
console.info("Server said: " + r.title);
console.info(r.description);
} catch(e) {
console.info("Server said: " + xhr2.statusText);
}
}
};
xhr2.send(forge.pki.certificationRequestToPem(csr));
}
}
xhr.send();
}
function onHashChanged() { function onHashChanged() {
var query = {}; window.query = {};
var a = location.hash.substring(1).split('&'); var a = location.hash.substring(1).split('&');
for (var i = 0; i < a.length; i++) { for (var i = 0; i < a.length; i++) {
var b = a[i].split('='); var b = a[i].split('=');
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);
if (window.location.protocol != "https:") { if (window.location.protocol != "https:") {
$.get("/api/certificate/", function(blob) { $.get("/api/certificate/", function(blob) {
$("#view-dashboard").html(env.render('views/insecure.html', { window: window, $("#view-dashboard").html(env.render('views/insecure.html', { window: window,
authority_name: window.location.hostname, session: { authority: {
session: { authority: { certificate: { blob: blob }}} hostname: window.location.hostname,
certificate: { blob: blob }}}
})); }));
}); });
} else { } else {
loadAuthority(query); if (query.action == "enroll") {
$("#view-dashboard").html(env.render('views/enroll.html'));
var options = document.querySelectorAll(".option");
for (i = 0; i < options.length; i++) {
options[i].style.display = "none";
}
setTimeout(onKeyGen, 100);
console.info("Generating key pair...");
} else {
loadAuthority(query);
}
} }
} }
function onTagClicked(tag) { function onTagClicked(e) {
var cn = $(tag).attr("data-cn"); e.preventDefault();
var id = $(tag).attr("title"); var cn = $(e.target).attr("data-cn");
var value = $(tag).html(); var id = $(e.target).attr("title");
var value = $(e.target).html();
var updated = prompt("Enter new tag or clear to remove the tag", value); var updated = prompt("Enter new tag or clear to remove the tag", value);
if (updated == "") { if (updated == "") {
$(event.target).addClass("disabled"); $(event.target).addClass("disabled");
@ -57,7 +229,7 @@ function onTagClicked(tag) {
url: "/api/signed/" + cn + "/tag/" + id + "/" url: "/api/signed/" + cn + "/tag/" + id + "/"
}); });
} else if (updated && updated != value) { } else if (updated && updated != value) {
$(tag).addClass("disabled"); $(e.target).addClass("disabled");
$.ajax({ $.ajax({
method: "PUT", method: "PUT",
url: "/api/signed/" + cn + "/tag/" + id + "/", url: "/api/signed/" + cn + "/tag/" + id + "/",
@ -77,9 +249,10 @@ function onTagClicked(tag) {
return false; return false;
} }
function onNewTagClicked(menu) { function onNewTagClicked(e) {
var cn = $(menu).attr("data-cn"); e.preventDefault();
var key = $(menu).attr("data-key"); var cn = $(e.target).attr("data-cn");
var key = $(e.target).attr("data-key");
var value = prompt("Enter new " + key + " tag for " + cn); var value = prompt("Enter new " + key + " tag for " + cn);
if (!value) return; if (!value) return;
if (value.length == 0) return; if (value.length == 0) return;
@ -101,6 +274,7 @@ function onNewTagClicked(menu) {
alert(e); alert(e);
} }
}); });
return false;
} }
function onTagFilterChanged() { function onTagFilterChanged() {
@ -121,6 +295,7 @@ function onLogEntry (e) {
message: e.message, message: e.message,
severity: e.severity, severity: e.severity,
fresh: e.fresh, fresh: e.fresh,
keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|")
} }
})); }));
} }
@ -270,7 +445,7 @@ function onServerStopped() {
} }
function onSendToken() { function onIssueToken() {
$.ajax({ $.ajax({
method: "POST", method: "POST",
url: "/api/token/", url: "/api/token/",
@ -316,20 +491,16 @@ function loadAuthority(query) {
console.info("Loaded:", session); console.info("Loaded:", session);
$("#login").hide(); $("#login").hide();
$("#search").show();
if (!query.columns) {
query.columns = 2;
}
/** /**
* Render authority views * Render authority views
**/ **/
$("#view-dashboard").html(env.render('views/authority.html', { $("#view-dashboard").html(env.render('views/authority.html', {
session: session, session: session,
window: window, window: window
columns: query.columns, }));
column_width: 12 / query.columns,
authority_name: window.location.hostname }));
$("time").timeago(); $("time").timeago();
if (session.authority) { if (session.authority) {
$("#log input").each(function(i, e) { $("#log input").each(function(i, e) {
@ -414,12 +585,8 @@ function loadAuthority(query) {
$("#search").on("keyup", function() { $("#search").on("keyup", function() {
if (window.searchTimeout) { clearTimeout(window.searchTimeout); } if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
console.info("Setting timeout", window.searchTimeout);
}); });
console.log("Features enabled:", session.features);
if (session.request_submission_allowed) { if (session.request_submission_allowed) {
$("#request_submit").click(function() { $("#request_submit").click(function() {
$(this).addClass("busy"); $(this).addClass("busy");
@ -442,11 +609,9 @@ function loadAuthority(query) {
alert(e); alert(e);
} }
}); });
}); });
} }
$("nav .nav-link.dashboard").removeClass("disabled").click(function() { $("nav .nav-link.dashboard").removeClass("disabled").click(function() {
$("#column-requests").show(); $("#column-requests").show();
$("#column-signed").show(); $("#column-signed").show();
@ -458,31 +623,36 @@ function loadAuthority(query) {
* Fetch log entries * Fetch log entries
*/ */
if (session.features.logging) { if (session.features.logging) {
if (query.columns == 4) { if ($("#column-log:visible").length) {
loadLog(); loadLog();
} else {
$("nav .nav-link.log").removeClass("disabled").click(function() {
$("#column-requests").show();
$("#column-signed").show();
$("#column-revoked").show();
$("#column-log").hide();
});
} }
$("nav .nav-link.log").removeClass("disabled").click(function() {
loadLog();
$("#column-requests").show();
$("#column-signed").show();
$("#column-revoked").show();
$("#column-log").hide();
});
} else {
console.info("Log disabled");
} }
} }
}); });
} }
function loadLog() { function loadLog() {
if (window.log_initialized) return; if (window.log_initialized) {
console.info("Log already loaded");
return;
}
console.info("Loading log...");
window.log_initialized = true; window.log_initialized = true;
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "/api/log/", url: "/api/log/?limit=100",
dataType: "json", dataType: "json",
success: function(entries, status, xhr) { success: function(entries, status, xhr) {
console.info("Got", entries.length, "log entries"); console.info("Got", entries.length, "log entries");
console.info("j=", entries.length-1);
for (var j = entries.length-1; j--; ) { for (var j = entries.length-1; j--; ) {
onLogEntry(entries[j]); onLogEntry(entries[j]);
}; };

View File

@ -0,0 +1,15 @@
- hosts: {% for router in session.service.routers %}
{{ router }}{% endfor %}
roles:
- role: certidude
authority_name: {{ session.authority.hostname }}
- role: ipsec_mesh
mesh_name: mymesh
authority_name: {{ session.authority.hostname }}
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 %}!
auto: start
nodes:{% for router in session.service.routers %}
{{ router }}: 172.27.{{ loop.index }}.0/24{% endfor %}

View File

@ -1,25 +1,24 @@
pip3 install git+https://github.com/laurivosandi/certidude/ pip3 install git+https://github.com/laurivosandi/certidude/
mkdir -p /etc/certidude/{client.conf.d,services.conf.d} mkdir -p /etc/certidude/{client.conf.d,services.conf.d}
cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf
[{{ authority_name }}] cat << \EOF > /etc/certidude/client.conf.d/{{ session.authority.hostname }}.conf
[{{ session.authority.hostname }}]
trigger = interface up trigger = interface up
common name = $HOSTNAME common name = $HOSTNAME
system wide = true system wide = true
EOF EOF
cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
[IPSec to {{ router }}] [IPSec to {{ router }}]
authority = {{ authority_name }} authority = {{ session.authority.hostname }}
service = network-manager/strongswan service = network-manager/strongswan
remote = {{ router }} remote = {{ router }}
{% endif %}{% if "openvpn" in session.service.protocols %} {% endif %}{% if "openvpn" in session.service.protocols %}
[OpenVPN to {{ router }}] [OpenVPN to {{ router }}]
authority = {{ authority_name }} authority = {{ session.authority.hostname }}
service = network-manager/openvpn service = network-manager/openvpn
remote = {{ router }} remote = {{ router }}
{% endif %}{% endfor %} {% endif %}{% endfor %}EOF
EOF
certidude enroll certidude enroll

View File

@ -1,8 +1,8 @@
# Create VPN gateway up/down script for reporting client IP addresses to CA # Create VPN gateway up/down script for reporting client IP addresses to CA
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown cat <<\EOF > /etc/certidude/authority/{{ session.authority.hostname }}/updown
#!/bin/sh #!/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/" CURL="curl -m 3 -f --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem https://{{ session.authority.hostname }}:8443/api/lease/"
case $PLUTO_VERB in 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" ;; up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
@ -15,5 +15,5 @@ case $script_type in
esac esac
EOF EOF
chmod +x /etc/certidude/authority/{{ authority_name }}/updown chmod +x /etc/certidude/authority/{{ session.authority.hostname }}/updown

View File

@ -0,0 +1,97 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html -->
<key>PayloadDisplayName</key>
<string>{{ gateway }}</string>
<!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
<key>PayloadIdentifier</key>
<string>org.example.vpn2</string>
<key>PayloadUUID</key>
<string>9f93912b-5fd2-4455-99fd-13b9a47b4581</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadIdentifier</key>
<string>org.example.vpn2.conf1</string>
<key>PayloadUUID</key>
<string>29e4456d-3f03-4f15-b46f-4225d89465b7</string>
<key>PayloadType</key>
<string>com.apple.vpn.managed</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>UserDefinedName</key>
<string>{{ gateway }}</string>
<key>VPNType</key>
<string>IKEv2</string>
<key>IKEv2</key>
<dict>
<key>RemoteAddress</key>
<string>{{ gateway }}</string>
<key>RemoteIdentifier</key>
<string>{{ gateway }}</string>
<key>LocalIdentifier</key>
<string>{{ common_name }}</string>
<key>ServerCertificateIssuerCommonName</key>
<string>{{ session.authority.certificate.common_name }}</string>
<key>ServerCertificateCommonName</key>
<string>{{ gateway }}</string>
<key>AuthenticationMethod</key>
<string>Certificate</string>
<key>IKESecurityAssociationParameters</key>
<dict>
<key>EncryptionAlgorithm</key>
<string>AES-256</string>
<key>IntegrityAlgorithm</key>
<string>SHA2-384</string>
<key>DiffieHellmanGroup</key>
<integer>14</integer>
</dict>
<key>ChildSecurityAssociationParameters</key>
<dict>
<key>EncryptionAlgorithm</key>
<string>AES-128-GCM</string>
<key>IntegrityAlgorithm</key>
<string>SHA2-256</string>
<key>DiffieHellmanGroup</key>
<integer>14</integer>
</dict>
<key>EnablePFS</key>
<integer>1</integer>
<key>PayloadCertificateUUID</key>
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
</dict>
</dict>
<dict>
<key>PayloadIdentifier</key>
<string>ee.k-space.ca2.client</string>
<key>PayloadUUID</key>
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
<key>PayloadType</key>
<string>com.apple.security.pkcs12</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadContent</key>
<data>{{ p12 }}</data>
</dict>
<dict>
<key>PayloadIdentifier</key>
<string>org.example.ca</string>
<key>PayloadUUID</key>
<string>64988b2c-33e0-4adf-a432-6fbcae543408</string>
<key>PayloadType</key>
<string>com.apple.security.root</string>
<key>PayloadVersion</key>
<integer>1</integer>
<!-- This is the Base64 (PEM) encoded CA certificate -->
<key>PayloadContent</key>
<data>{{ ca }}</data>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,35 @@
client
nobind{% for router in session.service.routers %}
remote {{ router }}{% endfor %}
proto tcp-client
port 443
tls-version-min 1.2
tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-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
{% if ca %}
<ca>
{{ ca }}
</ca>
{% else %}ca /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %}
{% if key %}
<key>
{{ key }}
</key>
{% else %}key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %}
{% if cert %}
<cert>
{{ cert }}
</cert>
{% else %}cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %}
# To enable dynamic DNS server update on Ubuntu, uncomment these
#script-security 2
#up /etc/openvpn/update-resolv-conf
#down /etc/openvpn/update-resolv-conf

View File

@ -2,26 +2,19 @@
which apt && apt install openvpn which apt && apt install openvpn
which dnf && dnf install openvpn which dnf && dnf install openvpn
cat > /etc/openvpn/{{ authority_name }}.conf << EOF # Create OpenVPN configuration file
client cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF
nobind {% include "snippets/openvpn-client.conf" %}
{% 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 EOF
# Restart OpenVPN service
systemctl restart openvpn systemctl restart openvpn
{#
Some notes:
- Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM
- NetworkManager's OpenVPN profile importer doesn't understand multiple remotes
- Tunnelblick and OpenVPN Connect apps don't have a method to update CRL
#}

View File

@ -65,9 +65,9 @@ for section in s2c_tcp s2c_udp; do
# Common paths # Common paths
uci set openvpn.$section.script_security=2 uci set openvpn.$section.script_security=2
uci set openvpn.$section.client_connect='/etc/certidude/updown' 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.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem' uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem'
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem' uci set openvpn.$section.ca='/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem'
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %} {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
uci set openvpn.$section.enabled=1 uci set openvpn.$section.enabled=1

View File

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

View File

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

View File

@ -1,14 +1,16 @@
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \ # Delete CA certificate if checksum doesn't match
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ session.authority.hostname }}/*.pem
{% include "snippets/store-authority.sh" %} {% include "snippets/store-authority.sh" %}
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \ {% include "snippets/update-trust.sh" %}
# Generate private key
test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ || {% 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/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %} -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \ test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \ || openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ -key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem -out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:" echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem cat /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem

View File

@ -1,13 +1,12 @@
# Use fully qualified name
test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) 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 -e /bin/hostname && NAME=$(hostname -f)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
{% include "snippets/update-trust.sh" %}
{% include "snippets/request-common.sh" %} {% include "snippets/request-common.sh" %}
# Submit CSR and save signed certificate
curl -f -L -H "Content-type: application/pkcs10" \ curl --cert-status -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes' 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes'

View File

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

View File

@ -1,10 +1,10 @@
cat > /etc/ipsec.conf << EOF cat > /etc/ipsec.conf << EOF
config setup
strictcrlpolicy=yes
ca {{ authority_name }} ca {{ session.authority.hostname }}
auto=add auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem cacert=/etc/certidude/authority/{{ session.authority.hostname }}/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 conn client-to-site
auto=start auto=start
@ -12,7 +12,7 @@ conn client-to-site
rightsubnet=0.0.0.0/0 rightsubnet=0.0.0.0/0
rightca="{{ session.authority.certificate.distinguished_name }}" rightca="{{ session.authority.certificate.distinguished_name }}"
left=%defaultroute left=%defaultroute
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
leftsourceip=%config leftsourceip=%config
leftca="{{ session.authority.certificate.distinguished_name }}" leftca="{{ session.authority.certificate.distinguished_name }}"
keyexchange=ikev2 keyexchange=ikev2
@ -21,9 +21,8 @@ conn client-to-site
closeaction=restart closeaction=restart
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 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 %}! esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
EOF EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets
ipsec restart ipsec restart apparmor

View File

@ -6,11 +6,12 @@ test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
# Set SELinux context # 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/{{ session.authority.hostname }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ session.authority.hostname }}.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/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.pem
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem /etc/ipsec.d/private/{{ session.authority.hostname }}.pem
# Patch AppArmor # Patch AppArmor
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
/etc/certidude/authority/** /etc/certidude/authority/** r,
EOF EOF
systemctl restart

View File

@ -4,19 +4,17 @@ config setup
strictcrlpolicy=yes strictcrlpolicy=yes
uniqueids=yes uniqueids=yes
ca {{ authority_name }} ca {{ session.authority.hostname }}
auto=add auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem cacert=/etc/certidude/authority/{{ session.authority.hostname }}/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 }} conn default-{{ session.authority.hostname }}
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 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 %}! esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
left=$(uci get network.wan.ipaddr) # Bind to this IP address left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftid={{ session.service.routers | first }} leftid={{ session.service.routers | first }}
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors 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 leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
leftca="{{ session.authority.certificate.distinguished_name }}" leftca="{{ session.authority.certificate.distinguished_name }}"
@ -27,15 +25,15 @@ conn default-{{ authority_name }}
conn site-to-clients conn site-to-clients
auto=add auto=add
also=default-{{ authority_name }} also=default-{{ session.authority.hostname }}
conn site-to-client1 conn site-to-client1
auto=ignore auto=ignore
also=default-{{ authority_name }} also=default-{{ session.authority.hostname }}
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*" rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
rightsourceip=172.21.0.1 rightsourceip=172.21.0.1
EOF EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem" > /etc/ipsec.secrets

View File

@ -1,7 +1,9 @@
# Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium
test -e /etc/pki/ca-trust/source/anchors \ 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 }} \ && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ session.authority.hostname }} \
&& update-ca-trust && 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
# Insert into Ubuntu trust store, only applies to curl
test -e /usr/local/share/ca-certificates/ \
&& ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /usr/local/share/ca-certificates/{{ session.authority.hostname }}.crt \
&& update-ca-certificates

View File

@ -1,7 +1,6 @@
# Install CA certificate # Install CA certificate
@" @"
{{ session.authority.certificate.blob }} {{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem
"@ | Out-File ca_cert.pem
{% if session.authority.certificate.algorithm == "ec" %} {% if session.authority.certificate.algorithm == "ec" %}
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
{% else %} {% else %}
@ -25,25 +24,25 @@ KeyAlgorithm = ECDSA_P384
KeyLength = 2048 KeyLength = 2048
{% endif %}"@ | Out-File req.inf {% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem 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 Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
# Import certificate # Import certificate
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My {% 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 {% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
{% endif %} {% endif %}
# Set up IPSec VPN tunnel
Remove-VpnConnection -AllUserConnection -Force k-space {% for router in session.service.routers %}
# Set up IPSec VPN tunnel to {{ router }}
Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}"
Add-VpnConnection ` Add-VpnConnection `
-Name k-space ` -Name "IPSec to {{ router }}" `
-ServerAddress guests.k-space.ee ` -ServerAddress {{ router }} `
-AuthenticationMethod MachineCertificate ` -AuthenticationMethod MachineCertificate `
-SplitTunneling ` -SplitTunneling `
-TunnelType ikev2 ` -TunnelType ikev2 `
-PassThru -AllUserConnection -PassThru -AllUserConnection
# Security hardening
Set-VpnConnectionIPsecConfiguration ` Set-VpnConnectionIPsecConfiguration `
-ConnectionName k-space ` -ConnectionName "IPSec to {{ router }}" `
-AuthenticationTransformConstants GCMAES128 ` -AuthenticationTransformConstants GCMAES128 `
-CipherTransformConstants GCMAES128 ` -CipherTransformConstants GCMAES128 `
-EncryptionMethod AES256 ` -EncryptionMethod AES256 `
@ -51,6 +50,8 @@ Set-VpnConnectionIPsecConfiguration `
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` -DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` -PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
-PassThru -AllUserConnection -Force -PassThru -AllUserConnection -Force
{% endfor %}
{# {#
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 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 CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256

View File

@ -5,51 +5,97 @@
<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"> <div class="modal-body">
<div class="modal-body"> <ul class="nav nav-pills" id="myTab" role="tablist">
<h5>Certidude client</h5> <li class="nav-item">
<p>On Ubuntu or Fedora:</p> <a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a>
<div class="highlight"> </li>
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
</div>
{% if "ikev2" in session.service.protocols %} <li class="nav-item">
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5> <a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a>
<p>On Windows execute following PowerShell script</p> </li>
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
{% endif %}
<h5>UNIX & UNIX-like</h5> <li class="nav-item">
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p> <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a>
<div class="highlight"> </li>
<pre class="code"><code>{% include "snippets/request-client.sh" %}</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>{% include "snippets/request-server.sh" %}</code></pre>
</div>
<p>To renew:</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
</div>
{% if "openvpn" in session.service.protocols %} {% if "openvpn" in session.service.protocols %}
<h5>OpenVPN as client</h5> <li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-openvpn" role="tab" aria-controls="openvpn" aria-selected="false">OpenVPN</a>
<p>First acquire certificates using the snippet above.</p> </li>
<p>Then install software:</p>
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
{% endif %} {% endif %}
{% if "ikev2" in session.service.protocols %} {% if "ikev2" in session.service.protocols %}
<h5>StrongSwan as client</h5> <li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-ansible" role="tab" aria-controls="ansible" aria-selected="false">Ansible</a>
</li>
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a>
</li>
{% if session.authorization.scep_subnets %}
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-scep" role="tab" aria-controls="scep" aria-selected="false">SCEP</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<!-- Certidude client -->
<div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude">
<p>On Ubuntu or Fedora:</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
</div>
</div>
<!-- Windows -->
<div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows">
<p>On Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %} execute following PowerShell script</p>
{% if "ikev2" in session.service.protocols %}
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
{% endif %}
</div>
<!-- UNIX-like -->
<div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix">
<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>{% include "snippets/request-client.sh" %}</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>{% include "snippets/request-server.sh" %}</code></pre>
</div>
<p>To renew:</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
</div>
</div>
<!-- OpenVPN as client -->
<div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn">
<p>First acquire certificates using the snippet above.</p>
<p>Then install software:</p>
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
</div>
<!-- StrongSwan as client -->
<div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan">
<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>
@ -59,68 +105,67 @@
<p>To configure StrongSwan as roadwarrior:</p> <p>To configure StrongSwan as roadwarrior:</p>
<div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div> <div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
{% endif %} </div>
<!-- Ansible -->
<div class="tab-pane fade" id="snippet-ansible" role="tabpanel" aria-labelledby="ansible">
<p>Fetch Ansible roles from https://github.com/laurivosandi/certidude-ansible</p>
<p>In your site.yml add:</p>
<div class="highlight"><pre class="code"><code>{% include "snippets/ansible-site.yml" %}</code></pre></div>
</div>
<h5>OpenWrt/LEDE as VPN gateway</h5> <!-- LEDE -->
<div class="tab-pane fade" id="snippet-lede" role="tabpanel" aria-labelledby="lede">
<p>To enroll from OpenWrt/LEDE and to set it up as OpenVPN/IKEv2 gateway,
first enroll certificates using the snippet from UNIX section above</p>
<p>First enroll certificates using the snippet from UNIX section above</p> <p>Then:</p>
<div class="highlight">
<p>Then:</p> <pre class="code"><code>opkg install curl libmbedtls
<div class="highlight"> # Derive FQDN from WAN interface's reverse DNS record
<pre class="code"><code>opkg install curl libmbedtls FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
# Derive FQDN from WAN interface's reverse DNS record grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
{% include "snippets/gateway-updown.sh" %} {% include "snippets/gateway-updown.sh" %}
</code></pre> </code></pre>
</div>
{% if "openvpn" in session.service.protocols %}
<p>Then either set up OpenVPN service:</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
</div>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<p>Alternatively or additionally set up StrongSwan:</p>
<div class="highlight">
<pre class="code"><code>opkg update
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
{% include "snippets/strongswan-server.sh" %}
ipsec restart</code></pre>
</div>
{% endif %}
</div> </div>
{% if "openvpn" in session.service.protocols %} <!-- Copy & paste -->
<p>Then either set up OpenVPN service:</p> <div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste">
<div class="highlight"> <p>Use whatever tools you have available on your platform to generate
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre> keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
</div>
{% endif %}
{% if "ikev2" in session.service.protocols %} <form action="/api/request/" method="post">
<p>Alternatively or additionally set up StrongSwan:</p> <textarea id="request_body" style="width:100%; min-height: 10em;"
<div class="highlight"> placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
<pre class="code"><code>opkg update <div class="modal-footer">
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm <div class="btn-group">
{% include "snippets/strongswan-server.sh" %} <button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
ipsec restart</code></pre> <button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
</div> </div>
{% endif %} </div>
</form>
{% if session.authority.builder %}
<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>
<ul>
{% for name, title, filename in session.authority.builder.profiles %}
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
{% endfor %}
</ul>
{% endif %}
<h5>SCEP</h5>
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
<h5>Copy & paste</h5>
<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>
<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> </div>
</form> </div>
</div> </div>
</div> </div>
</div> </div>
@ -133,10 +178,10 @@
<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://{{ session.authority.hostname }}/api/revoked/">certificate revocation list</a>:</p>
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der <pre><code>curl http://{{ session.authority.hostname }}/api/revoked/ > crl.der
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file" curl http://{{ session.authority.hostname }}/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://{{ session.authority.hostname }}/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>
@ -146,11 +191,17 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-{{ column_width }}"> <div class="col-sm-6 col-lg-4 col-xl-3">
<h1>Signed certificates</h1> <h1>Signed certificates</h1>
<p>Authority administration allowed for <p>Authority administration
{% 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 %} {% if session.authority.certificate.organization %}of {{ session.authority.certificate.organization }}{% endif %}
{% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. allowed for
{% for user in session.authorization.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.authorization.admin_subnets %}anywhere{% else %}
{% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
Authority valid from
<time class="timeago" datetime="{{ session.authority.certificate.signed }}">{{ session.authority.certificate.signed }}</time>
until
<time class="timeago" datetime="{{ session.authority.certificate.expires }}">{{ session.authority.certificate.expires }}</time>.
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>. Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
Following certificates have been signed:</p> Following certificates have been signed:</p>
<div id="signed_certificates"> <div id="signed_certificates">
@ -159,7 +210,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="col-sm-{{ column_width }}"> <div class="col-sm-6 col-lg-4 col-xl-3">
{% if session.authority %} {% if session.authority %}
{% if session.features.token %} {% if session.features.token %}
<h1>Tokens</h1> <h1>Tokens</h1>
@ -174,51 +225,89 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
<input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2"> <input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2">
<input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2"> <input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2">
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-secondary" type="button" onClick="onSendToken();"><i class="fa fa-send"></i> Send token</button> <button class="btn btn-secondary" type="button" onClick="onIssueToken();"><i class="fa fa-send"></i> Send token</button>
</span> </span>
</div> </div>
</p> </p>
<p>Issued tokens:</p>
<ul>
{% for token in session.authority.tokens %}
<li>
<a href="mailto:{{ token.mail }}">{{ token.subject }}</a>
{% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %},
expires
<time class="timeago" datetime="{{ token.expires }}">{{ token.expires }}</time>
</li>
{% endfor %}
</ul>
<div id="token_qrcode"></div> <div id="token_qrcode"></div>
{% endif %} {% endif %}
<h1>Pending requests</h1> {% if session.authorization.request_subnets %}
<h1>Pending requests</h1>
<p>Use Certidude client to apply for a certificate. <p>Use Certidude client to apply for a certificate.
{% if not session.authority.request_subnets %} {% if not session.authorization.request_subnets %}
Request submission disabled. Request submission disabled.
{% elif "0.0.0.0/0" in session.authority.request_subnets %} {% elif "0.0.0.0/0" in session.authorization.request_subnets %}
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 %} {% for subnet in session.authorization.request_subnets %}
{{ subnet }}{% if not loop.last %}, {% endif %} {{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %}. {% endfor %}.
{% endif %} {% endif %}
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload. See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
{% if session.authority.autosign_subnets %} {% if session.authorization.autosign_subnets %}
{% if "0.0.0.0/0" in session.authority.autosign_subnets %} {% if "0.0.0.0/0" in session.authorization.autosign_subnets %}
All requests are automatically signed. All requests are automatically signed.
{% else %}
Requests from
{% for subnet in session.authorization.autosign_subnets %}
{{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %}
are automatically signed.
{% endif %}
{% endif %}
{% if session.authorization.scep_subnets %}
To enroll via SCEP from
{% if "0.0.0.0/0" in session.authorization.scep_subnets %}
anywhere
{% else %} {% else %}
Requests from {% for subnet in session.authorization.scep_subnets %}
{% for subnet in session.authority.autosign_subnets %} {{ subnet }}{% if not loop.last %}, {% endif %}
{{ subnet }}{% if not loop.last %}, {% endif %} {% endfor %}
{% endfor %}
are automatically signed.
{% endif %} {% endif %}
use http://{{ session.authority.hostname }}/cgi-bin/pkiclient.exe as the enrollment URL.
{% endif %}
</p>
<div id="pending_requests">
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
{% include "views/request.html" %}
{% endfor %}
</div>
{% endif %} {% endif %}
</p>
<div id="pending_requests"> {% if session.builder.profiles %}
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} <h2>LEDE imagebuilder</h2>
{% include "views/request.html" %} <p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
{% endfor %} <ul>
</div> {% for name, title, filename in session.builder.profiles %}
{% if columns >= 3 %} <li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div> </div>
<div class="col-sm-{{ column_width }}"> <div class="col-sm-6 col-lg-4 col-xl-3">
{% endif %}
<h1>Revoked certificates</h1> <h1>Revoked certificates</h1>
<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
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
@ -227,7 +316,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
{% 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 id="column-log" class="col-sm-6 col-lg-4 col-xl-3 hidden-lg-down">
<div class="loader-container"> <div class="loader-container">
<div class="loader"></div> <div class="loader"></div>
<p>Loading logs, this might take a while...</p> <p>Loading logs, this might take a while...</p>
@ -235,14 +324,15 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
<div class="content" style="display:none;"> <div class="content" style="display:none;">
<h1>Log</h1> <h1>Log</h1>
<div class="btn-group" data-toggle="buttons"> <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-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-error" type="checkbox" autocomplete="off" checked>Error</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-warning" type="checkbox" autocomplete="off" checked>Warn</label>
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</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> <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 id="log-entries" class="list-group">
</ul> </ul>
<p>Click here to load more entries</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,238 @@
<!-- https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#Certificate-authentication -->
<!--
Browser status
- Edge doesn't work because they think data: urls are insecure
- iphone QR code scanner's webview is constrained, cant download data: links
- outlook.com via iphone mail client works
- android gmail app works
- chrome works
- firefox works
OS/soft status
- OpenVPN works on everything
- StrongSwan app works on Android
- NetworkManager doesn't support importing .sswan files yet, so no IPSec support for Ubuntu or Fedora here yet
-->
<div id="enroll" class="row">
<div class="loader-container">
<div class="loader"></div>
<p>Generating RSA keypair, this will take a while...</p>
</div>
<div class="col-sm-12 mt-3 edge-broken" style="display:none;">
<!-- https://stackoverflow.com/questions/33154646/data-uri-link-a-href-data-doesnt-work-in-microsoft-edge?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa -->
Microsoft Edge not supported, open the link with Chrome or Firefox
</div>
<div class="col-sm-12 mt-3 option ubuntu linux openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">Ubuntu 16.04+</h3>
<p class="card-text">Install OpenVPN plugin for NetworkManager by executing following two command in the terminal:
<pre><code># Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo
if [ $(lsb_relase -cs) == "xenial" ]; then
wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list
apt update
apt install openvpn
fi
sudo apt install -y network-manager-openvpn-gnome
sudo systemctl restart network-manager
</code></pre>
<p>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#ubuntu-screenshots" aria-expanded="false" aria-controls="ubuntu-screenshots">
Screenshots
</button>
</p>
<div class="collapse" id="ubuntu-screenshots">
<p>Open up network connections:</p>
<p><img src="/img/ubuntu-01-edit-connections.png"/></p>
<p>Hit <i>Add button</i>:</p>
<p><img src="/img/ubuntu-02-network-connections.png"/></p>
<p>Select <i>Import a saved VPN configuration...</i>:</p>
<p><img src="/img/ubuntu-03-import-saved-config.png"/></p>
<p>Select downloaded file:</p>
<p><img src="/img/ubuntu-04-select-file.png"/></p>
<p>Once profile is successfully imported following dialog appears:</p>
<p><img src="/img/ubuntu-05-profile-imported.png"/></p>
<p>By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select <i>Routes...</i> under <i>IPv4 Settings</i>:</p>
<p><img src="/img/ubuntu-06-ipv4-settings.png"/></p>
<p>Check <i>Use this connection only for resources on its network</i>:</p>
<p><img src="/img/ubuntu-07-disable-default-route.png"/></p>
<p>To activate the connection select it under <i>VPN Connections</i>:</p>
<p><img src="/img/ubuntu-08-activate-connection.png"/></p>
</div>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option fedora linux openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">Fedora</h3>
<p class="card-text">Install OpenVPN plugin for NetworkManager by running following two commands:</p>
<pre><code>dnf install NetworkManager-openvpn-gnome
systemctl restart NetworkManager</code></pre>
Right click in the NetworkManager icon, select network settings. Hit the + button and select <i>Import from file...</i>, select the downloaded .ovpn file.
Remove the .ovpn file from the Downloads folder.</p>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option windows ipsec">
<div class="card">
<div class="card-block">
<h3 class="card-title">Windows</h3>
<p class="card-text">
Import PKCS#12 container to your machine trust store.
Import VPN connection profile by moving the downloaded .pbk file to
<pre><code>%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK</code></pre>
or
<pre><code>C:\ProgramData\Microsoft\Network\Connections\Pbk</code></pre></p>
<a href="javascript:onEnroll('p12');" class="btn btn-primary">Fetch PKCS#12 container</a>
<a href="#" class="btn btn-secondary">Fetch VPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option windows openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">Windows</h3>
<p class="card-text">
Install OpenVPN community edition client.
Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and
right click in the system tray on OpenVPN icon and select Connect from the menu.
For finishing touch adjust the file permissions so only local
administrator can read that file, remove regular user access to the file.
</p>
<a href="https://openvpn.net/index.php/download/community-downloads.html" class="btn btn-secondary">Get OpenVPN community edition</a>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#windows-screenshots" aria-expanded="false" aria-controls="windows-screenshots">
Screenshots
</button>
<div class="collapse" id="windows-screenshots">
<p>Download OpenVPN from the link supplied above:</p>
<p><img src="/img/windows-01-download-openvpn.png"/></p>
<p>Install OpenVPN:</p>
<p><img src="/img/windows-02-install-openvpn.png"/></p>
<p>Move the configuraiton file downloaded from the second button above:</p>
<p><img src="/img/windows-03-move-config-file.png"/></p>
<p>Connect from system tray:</p>
<p><img src="/img/windows-04-connect.png"/></p>
<p>Connection is successfully configured:</p>
<p><img src="/img/windows-05-connected.png"/></p>
</div>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option mac openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">Mac OS X</h3>
<p class="card-text">Download Tunnelblick. Tap on the button above and import the profile.</p>
<a href="https://tunnelblick.net/" target="_blank" class="btn btn-secondary">Get Tunnelblick</a>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option iphone ipad openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">iPhone/iPad</h3>
<p class="card-text">Install OpenVPN Connect app, tap on the button below.</p>
<a href="https://itunes.apple.com/us/app/openvpn-connect/id590379981?mt=8" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option iphone ipad ikev2">
<div class="card">
<div class="card-block">
<h3 class="card-title">iPhone/iPad</h3>
<p class="card-text">
Tap the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
Hit <i>Install</i> in the top-right corner.
Enter your passcode to unlock trust store.
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
Where password for the certificate is prompted, enter 1234.
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
</p>
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option mac ikev2">
<div class="card">
<div class="card-block">
<h3 class="card-title">Mac OS X</h3>
<p class="card-text">
Click on the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
Hit <i>Install</i> in the top-right corner.
Enter your passcode to unlock trust store.
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
Where password for the certificate is prompted, enter 1234.
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
</p>
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option android openvpn">
<div class="card">
<div class="card-block">
<h3 class="card-title">Android</h3>
<p class="card-text">Intall OpenVPN Connect app on your device.
Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import.
Hit <i>Accept</i> and then <i>Connect</i>.
Remember to delete any remaining .ovpn files under the <i>Downloads</i>.
</p>
<a href="https://play.google.com/store/apps/details?id=net.openvpn.openvpn" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option android ikev2">
<div class="card">
<div class="card-block">
<h3 class="card-title">Android</h3>
<p class="card-text">
Install strongSwan Client app on your device.
Tap on the downloaded .sswan file, StrongSwan Client should prompt for import.
Hit <i>Import certificate from VPN profile</i> and then <i>Import</i> in the top-right corner.
Remember to delete any remaining .sswan files under the <i>Downloads</i>.
</p>
<a href="https://play.google.com/store/apps/details?id=org.strongswan.android" class="btn btn-secondary">Get strongSwan VPN Client app</a>
<a href="javascript:onEnroll('sswan');" class="btn btn-primary">Fetch StrongSwan profile</a>
</div>
</div>
</div>
<!--
<a href="javascript:onShowAll();">I did't find an appropriate option for me, show all options</a>
-->
</div>

View File

@ -1,5 +1,5 @@
<p>You're viewing this page over insecure channel. <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>, You can give it a try and <a href="https://{{ session.authority.hostname }}">connect over HTTPS</a>,
if that succeeds all subsequents accesses of this page will go over HTTPS. if that succeeds all subsequents accesses of this page will go over HTTPS.
</p> </p>
<p> <p>

View File

@ -1,4 +1,4 @@
<li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}"> <li id="log_entry_{{ entry.id }}" data-keywords="{{ entry.message }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}">
<span> <span>
<i class="fa fa-{{ entry.severity }}-circle"/> <i class="fa fa-{{ entry.severity }}-circle"/>
{{ entry.message }} {{ entry.message }}

View File

@ -40,8 +40,8 @@
<div class="collapse" id="details-{{ request.sha256sum }}"> <div class="collapse" id="details-{{ request.sha256sum }}">
<p>Use following to fetch the signing request:</p> <p>Use following to fetch the signing request:</p>
<div class="bd-example"> <div class="bd-example">
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/</a> <pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/</a>
curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/ \ curl http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \
| openssl req -text -noout</code></pre> | openssl req -text -noout</code></pre>
</div> </div>

View File

@ -29,15 +29,15 @@
<p>To fetch certificate:</p> <p>To fetch certificate:</p>
<div class="bd-example"> <div class="bd-example">
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/</a> <pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/</a>
curl http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/ \ curl http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \
| openssl x509 -text -noout</code></pre> | openssl x509 -text -noout</code></pre>
</div> </div>
<p>To perform online certificate status request</p> <p>To perform online certificate status request</p>
<pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem <pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate/ > session.pem
openssl ocsp -issuer session.pem -CAfile session.pem \ openssl ocsp -issuer session.pem -CAfile session.pem \
-url http://{{ window.location.hostname }}/api/ocsp/ \ -url http://{{ session.authority.hostname }}/api/ocsp/ \
-serial 0x{{ certificate.serial }}</span></code></pre> -serial 0x{{ certificate.serial }}</span></code></pre>
<p> <p>

View File

@ -56,7 +56,7 @@
<div class="btn-group"> <div class="btn-group">
{% if session.authority.tagging %} {% if session.authority.tagging %}
<button type="button" class="btn btn-default" onclick="onNewTagClicked(this);" data-key="other" data-cn="{{ certificate.common_name }}"> <button type="button" class="btn btn-default" onclick="onNewTagClicked(event);" data-key="other" data-cn="{{ certificate.common_name }}">
<i class="fa fa-tag"></i> Tag</button> <i class="fa fa-tag"></i> Tag</button>
<button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
@ -64,7 +64,7 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
{% for tag_category in session.authority.tagging %} {% for tag_category in session.authority.tagging %}
<a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}" <a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}"
onclick="onNewTagClicked(this);">{{ tag_category.title }}</a> onclick="onNewTagClicked(event);">{{ tag_category.title }}</a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -74,24 +74,29 @@
<p>To fetch certificate:</p> <p>To fetch certificate:</p>
<div class="bd-example"> <div class="bd-example">
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/</a> <pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/</a>
curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/ \ curl --cert-status http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/ \
| openssl x509 -text -noout</code></pre> | openssl x509 -text -noout</code></pre>
</div> </div>
{% if session.features.ocsp %} {% if session.authorization.ocsp_subnets %}
<p>To perform online certificate status request:</p> {% if certificate.responder_url %}
<pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem <p>To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %}
from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:</p>
<pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate > session.pem
openssl ocsp -issuer session.pem -CAfile session.pem \ openssl ocsp -issuer session.pem -CAfile session.pem \
-url http://{{ window.location.hostname }}/api/ocsp/ \ -url {{ certificate.responder_url }} \
-serial 0x{{ certificate.serial }}</code></pre> -serial 0x{{ certificate.serial }}</code></pre>
{% else %}
<p>Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired</p>
{% endif %}
{% endif %} {% endif %}
<p>To fetch script:</p> <p>To fetch script:</p>
<pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \ <pre><code class="language-bash" data-lang="bash">curl --cert-status https://{{ session.authority.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
--cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
--key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \ --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
--cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code> --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem</pre></code>
<div style="overflow: auto; max-width: 100%;"> <div style="overflow: auto; max-width: 100%;">
<table class="table" id="signed_certificates"> <table class="table" id="signed_certificates">
@ -112,8 +117,11 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
--> -->
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr> <tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
{% if certificate.extensions.extended_key_usage %} {% if certificate.key_usage %}
<tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr> <tr><th>Key usage</th><td>{{ certificate.key_usage | join(", ") | replace("_", " ") }}</td></tr>
{% endif %}
{% if certificate.extended_key_usage %}
<tr><th>Extended key usage</th><td>{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}</td></tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>

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);">{{ tag.value }}</span> onClick="onTagClicked(event);">{{ tag.value }}</span>
{% endfor %} {% endfor %}

View File

@ -1,9 +1,9 @@
Token for {{ user.name }} Token for {{ subject }}
{% if issuer == user %} {% if issuer == subject %}
Token has been issued for {{ user }} for retrieving profile from link below. Token has been issued for {{ subject }} for retrieving profile from link below.
{% else %} {% else %}
{{ issuer }} has provided {{ user }} a token for retrieving {{ issuer }} has provided {{ subject }} a token for retrieving
profile from the link below. profile from the link below.
{% endif %} {% endif %}

View File

@ -1,9 +1,24 @@
# Configure secure defaults for nginx
ssl_dhparam {{ dhparam_path }};
# Note that depending on the certificate type (RSA, ECDSA) this will be even further constrained:
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA512:DHE-ECDSA-AES256-GCM-SHA512:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_trusted_certificate {{ ca_cert }}; # OCSP responder trust chain
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
# Following are already enabled by /etc/nginx/nginx.conf # Following are already enabled by /etc/nginx/nginx.conf
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
#ssl_prefer_server_ciphers on; #ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_dhparam {{dhparam_path}};
# Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support # Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support
map $ssl_client_s_dn $ssl_client_s_dn_cn { map $ssl_client_s_dn $ssl_client_s_dn_cn {

View File

@ -1,13 +1,16 @@
[DEFAULT] [DEFAULT]
# LEDE image builder profiles enabled by default
enabled = yes
# Path to filesystem overlay used # Path to filesystem overlay used
overlay = {{ doc_path }}/overlay overlay = {{ doc_path }}/overlay
# Hostname or regex to match the IPSec gateway included in the image # Hostname or regex to match the IPSec gateway included in the image
router = ^router\d?\. router = ^(router|vpn|gw|gateway)\d*\.
# Site specific script to be copied to /etc/uci-defaults/99-site-script # Site specific script to be copied to /etc/uci-defaults/99-site-script
# use it to include SSH keys, set passwords, etc # use it to include SSH keys, set passwords, etc
script = script = /etc/certidude/script/site.sh
# Which subnets are routed to the tunnel # Which subnets are routed to the tunnel
subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
@ -16,37 +19,96 @@ subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
ike=aes256-sha384-{{ dhgroup }}! ike=aes256-sha384-{{ dhgroup }}!
esp=aes128gcm16-aes128gmac-{{ dhgroup }}! esp=aes128gcm16-aes128gmac-{{ dhgroup }}!
[tpl-archer-c7]
# Title shown in the UI
title = TP-Link Archer C7 (Access Point)
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary [tpl-wdr3600-factory]
command = {{ doc_path }}/builder/ap.sh enabled = no
# Title shown in the UI
title = TP-Link WDR3600 (Access Point), TFTP-friendly
# Script to build the image, copy file to /etc/certidude/script/ and make modifications as necessary
command = /srv/certidude/doc/builder/ap.sh
# Device/model/profile selection # Device/model/profile selection
model = archer-c7-v2 model = tl-wdr3600-v1
# File that will be picked from the bin/ folder # File that will be picked from the bin/ folder
filename = archer-c7-v2-squashfs-factory-eu.bin filename = tl-wdr3600-v1-squashfs-factory.bin
# And renamed to make it TFTP-friendly # And renamed to make it TFTP-friendly
rename = wdr4300v1_tp_recovery.bin
[tpl-wdr4300-factory]
enabled = no
title = TP-Link WDR4300 (Access Point), TFTP-friendly
command = /srv/certidude/doc/builder/ap.sh
model = tl-wdr4300-v1
filename = tl-wdr4300-v1-squashfs-factory.bin
rename = wdr4300v1_tp_recovery.bin
[tpl-archer-c7-factory]
enabled = no
title = TP-Link Archer C7 (Access Point), TFTP-friendly
command = {{ doc_path }}/builder/ap.sh
model = archer-c7-v2
filename = archer-c7-v2-squashfs-factory-eu.bin
rename = ArcherC7v2_tp_recovery.bin rename = ArcherC7v2_tp_recovery.bin
[cf-e380ac] [cf-e380ac-factory]
title = Comfast E380AC (Access Point) enabled = no
title = Comfast E380AC (Access Point), TFTP-friendly
command = {{ doc_path }}/builder/ap.sh command = {{ doc_path }}/builder/ap.sh
model = cf-e380ac-v2 model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin filename = cf-e380ac-v2-squashfs-factory.bin
rename = firmware_auto.bin rename = firmware_auto.bin
[ar150-mfp]
[tpl-wdr3600-sysupgrade]
;enabled = yes
title = TP-Link WDR3600 (Access Point)
command = /srv/certidude/doc/builder/ap.sh
model = tl-wdr3600-v1
filename = tl-wdr3600-v1-squashfs-sysupgrade.bin
rename = ap-tl-wdr3600-v1-squashfs-sysupgrade.bin
[tpl-wdr4300-sysupgrade]
;enabled = yes
title = TP-Link WDR4300 (Access Point)
command = /srv/certidude/doc/builder/ap.sh
model = tl-wdr4300-v1
filename = tl-wdr4300-v1-squashfs-sysupgrade.bin
rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin
[tpl-archer-c7-sysupgrade]
;enabled = yes
title = TP-Link Archer C7 (Access Point)
command = {{ doc_path }}/builder/ap.sh
model = archer-c7-v2
filename = archer-c7-v2-squashfs-factory-eu.bin
rename = ap-archer-c7-v2-squashfs-factory-eu.bin
[cf-e380ac-sysupgrade]
;enabled = yes
title = Comfast E380AC (Access Point)
command = {{ doc_path }}/builder/ap.sh
model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin
rename = ap-cf-e380ac-v2-squashfs-factory.bin
[ar150-mfp-sysupgrade]
;enabled = yes
title = GL.iNet GL-AR150 (MFP) title = GL.iNet GL-AR150 (MFP)
command = {{ doc_path }}/builder/mfp.sh command = {{ doc_path }}/builder/mfp.sh
model = gl-ar150 model = gl-ar150
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
rename = mfp-gl-ar150-squashfs-sysupgrade.bin rename = mfp-gl-ar150-squashfs-sysupgrade.bin
[ar150-cam] [ar150-cam-sysupgrade]
;enabled = yes
title = GL.iNet GL-AR150 (IP Camera) title = GL.iNet GL-AR150 (IP Camera)
command = {{ doc_path }}/builder/ipcam.sh command = {{ doc_path }}/builder/ipcam.sh
model = gl-ar150 model = gl-ar150

View File

@ -1,9 +1,3 @@
# To set up SSL certificates using Let's Encrypt run:
#
#
# Also uncomment URL rewriting and SSL configuration below
# Basic DoS prevention measures # Basic DoS prevention measures
limit_conn addr 10; limit_conn addr 10;
client_body_timeout 5s; client_body_timeout 5s;
@ -72,6 +66,9 @@ server {
# Uncomment following to enable HTTPS # Uncomment following to enable HTTPS
#rewrite ^/$ https://$server_name$request_uri? permanent; #rewrite ^/$ https://$server_name$request_uri? permanent;
access_log /var/log/nginx/certidude-plaintext-access.log;
error_log /var/log/nginx/certidude-plaintext-error.log;
} }
server { server {
@ -110,7 +107,7 @@ server {
alias /var/www/html/.well-known/; alias /var/www/html/.well-known/;
} }
{% if not push_server %} {% if not push_server %}
# Event stream for pushing events to web browsers # Event stream for pushing events to web browsers
location ~ "^/ev/sub/(.*)" { location ~ "^/ev/sub/(.*)" {
nchan_channel_id $1; nchan_channel_id $1;
@ -122,14 +119,17 @@ server {
nchan_channel_id $1; nchan_channel_id $1;
nchan_subscriber longpoll; nchan_subscriber longpoll;
} }
{% endif %} {% endif %}
access_log /var/log/nginx/certidude-frontend-access.log;
error_log /var/log/nginx/certidude-frontend-error.log;
} }
server { server {
# Section for certificate authenticated HTTPS clients, # Section for certificate authenticated HTTPS clients,
# for submitting information to CA eg. leases, # for submitting information to CA eg. leases,
# renewing certificates and # requesting/renewing certificates and
# for delivering scripts to clients # for delivering scripts to clients
server_name {{ common_name }}; server_name {{ common_name }};
@ -150,6 +150,9 @@ server {
nchan_channel_id $1; nchan_channel_id $1;
nchan_subscriber longpoll; nchan_subscriber longpoll;
} }
access_log /var/log/nginx/certidude-mutual-auth-access.log;
error_log /var/log/nginx/certidude-mutual-auth-error.log;
} }
{% if not push_server %} {% if not push_server %}
@ -167,6 +170,10 @@ server {
nchan_publisher; nchan_publisher;
nchan_channel_id $1; nchan_channel_id $1;
} }
access_log /var/log/nginx/certidude-push-access.log;
error_log /var/log/nginx/certidude-push-error.log;
} }
{% endif %} {% endif %}

View File

@ -1,4 +1,5 @@
[DEFAULT] [DEFAULT]
enabled = no
ou = ou =
lifetime = 120 lifetime = 120
ca = false ca = false
@ -6,7 +7,19 @@ common name = RE_COMMON_NAME
key usage = digital_signature key_encipherment key usage = digital_signature key_encipherment
extended key usage = extended key usage =
# Strongswan can automatically fetch CRL if
# CRL distribution point extension is included in the certificate
;revoked url =
revoked url = {{ revoked_url }}
# StrongSwan can automatically query OCSP responder if
# AIA extension includes OCSP responder URL
;responder url =
;responder url = no check
responder url = {{ responder_url }}
[ca] [ca]
enabled = yes
title = Certificate Authority title = Certificate Authority
common name = ^ca common name = ^ca
ca = true ca = true
@ -15,12 +28,14 @@ extended key usage =
lifetime = 1095 lifetime = 1095
[rw] [rw]
enabled = yes
title = Roadwarrior title = Roadwarrior
ou = Roadwarrior ou = Roadwarrior
common name = RE_HOSTNAME common name = RE_HOSTNAME
extended key usage = client_auth extended key usage = client_auth
[srv] [srv]
enabled = yes
title = Server title = Server
ou = Server ou = Server
common name = RE_FQDN common name = RE_FQDN
@ -28,6 +43,7 @@ lifetime = 120
extended key usage = server_auth client_auth extended key usage = server_auth client_auth
[gw] [gw]
enabled = yes
title = Gateway title = Gateway
ou = Gateway ou = Gateway
common name = RE_FQDN common name = RE_FQDN
@ -36,6 +52,7 @@ lifetime = 120
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
[ap] [ap]
enabled = no
title = Access Point title = Access Point
ou = Access Point ou = Access Point
common name = RE_HOSTNAME common name = RE_HOSTNAME
@ -43,6 +60,7 @@ lifetime = 120
extended key usage = client_auth extended key usage = client_auth
[mfp] [mfp]
enabled = no
title = Printers title = Printers
ou = MFP ou = MFP
common name = ^mfp\- common name = ^mfp\-
@ -50,9 +68,16 @@ lifetime = 120
extended key usage = client_auth extended key usage = client_auth
[cam] [cam]
enabled = no
title = Camera title = Camera
ou = IP Camera ou = IP Camera
common name = ^cam\- common name = ^cam\-
lifetime = 120 lifetime = 120
extended key usage = client_auth extended key usage = client_auth
[ocsp]
enabled = no
title = OCSP Responder
common name = ^ocsp
lifetime = 7
responder url = nocheck

View File

@ -68,6 +68,9 @@ ldap base = {{ base }}
ldap base = dc=example,dc=lan ldap base = dc=example,dc=lan
{% endif %} {% endif %}
ldap mail attribute = mail
;ldap mail attribute = otherMailbox
[authorization] [authorization]
# The authorization backend specifies how the users are authorized. # The authorization backend specifies how the users are authorized.
# In case of 'posix' simply group membership is asserted, # In case of 'posix' simply group membership is asserted,
@ -182,16 +185,6 @@ revocation list lifetime = 24
# URL where CA certificate can be fetched from # URL where CA certificate can be fetched from
authority certificate url = {{ certificate_url }} authority certificate url = {{ certificate_url }}
# Strongswan can automatically fetch CRL if
# CRL distribution point extension is included in the certificate
;revoked url =
revoked url = {{ revoked_url }}
# StrongSwan can automatically query OCSP responder if
# AIA extension includes OCSP responder URL
responder url =
;responder url = {{ responder_url }}
[push] [push]
# This should occasionally be regenerated # This should occasionally be regenerated
@ -242,8 +235,12 @@ expired dir = {{ directory }}/expired/
# and make sure Certidude machine doesn't try to accept mails. # and make sure Certidude machine doesn't try to accept mails.
# uncomment mail sender address to enable e-mails. # uncomment mail sender address to enable e-mails.
# Make sure used e-mail address is reachable for end users. # Make sure used e-mail address is reachable for end users.
name = Certificate management name = Certidude at {{ common_name }}
address = certificates@example.lan {% if domain %}
address = certificates@{{ domain }}
{% else %}
address = certificates@exaple.com
{% endif %}
[tagging] [tagging]
owner/string = Owner owner/string = Owner
@ -259,17 +256,20 @@ services template = {{ template_path }}/bootstrap.conf
[token] [token]
# Token mechanism allows authority administrator to send invites for users. # Token mechanism allows authority administrator to send invites for users.
# Token API call /api/token/ could be for example exposed on the internet via proxypass. # Backend for tokens, set none to disable
# Token mechanism disabled by setting URL setting to none ;backend =
;url = http://ca.example.com/ backend = sql
url =
# Token lifetime in minutes, 30 minutes by default. # Database path for SQL backend
database = sqlite://{{ directory }}/meta/db.sqlite
# URL format
url = {{ token_url }}
# Token lifetime in minutes, 48 hours by default.
# Note that code tolerates 5 minute clock skew. # Note that code tolerates 5 minute clock skew.
lifetime = 30 lifetime = 2880
# Secret for generating and validating tokens, regenerate occasionally
secret = {{ token_secret }}
[script] [script]
# Path to the folder with scripts that can be served to the clients, set none to disable scripting # Path to the folder with scripts that can be served to the clients, set none to disable scripting
@ -279,4 +279,4 @@ path = {{ script_dir }}
[service] [service]
protocols = ikev2 https openvpn protocols = ikev2 https openvpn
routers = ^router\d?\. routers = ^(router|vpn|gw|gateway)\d*\.

View File

@ -0,0 +1,34 @@
# Configure port tagging
uci set network.lan.ifname='eth0.3' # Protected network VLAN3 tagged
uci set network.guest.ifname='eth0.4' # Public network VLAN4 tagged
# Configure wireless networks
for band in 2ghz 5ghz; do
uci delete wireless.radio$band.disabled
uci set wireless.radio$band.country=EE
uci set wireless.guest$band=wifi-iface
uci set wireless.guest$band.network=guest
uci set wireless.guest$band.mode=ap
uci set wireless.guest$band.device=radio$band
uci set wireless.guest$band.encryption=none
uci set wireless.guest$band.ssid="k-space.ee guest"
uci set wireless.lan$band=wifi-iface
uci set wireless.lan$band.network=lan
uci set wireless.lan$band.mode=ap
uci set wireless.lan$band.device=radio$band
uci set wireless.lan$band.encryption=psk2+ccmp
uci set wireless.lan$band.ssid="k-space protected"
uci set wireless.lan$band.key="salakala"
done
# Add Lauri's Yubikey
cat > /etc/dropbear/authorized_keys << \EOF
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb4iqSrJrA13ygAZTZb6ElPsMXrlXXrztxt3bcKuEbAiWOm9lR17puRLMZbM2tvAW+iwsDHfQAs0E6HDprP68nt+SGkQvItUtYeJBWDI405DbRodmDMySahmb6o6S3sqI4vryydOg1G+Z0DITksZzp91Ow+C++emk6aqWfXh7xATexCvKphfwXrBL+MDIwx6drIiN0FD08yd/zxGAlcQpR8o6uecmXdk32wL5W3+qqwbJrLjZmOweij5KSXuEARuQhM20KXzYzzQIAKqhIoALRSEX31L0bwxOqfVaotzk4TWKJSeetEhBOd7PtH0ZrmOHF+B20Ym+V3UkRY5P4calF
EOF
# Set root password to 'salakala'
sed -i 's|^root::|root:$1$S0wGaZqK$fzEzb0WTC5.WHm2Fz9UI9.:|' /etc/shadow

85
certidude/tokens.py Normal file
View File

@ -0,0 +1,85 @@
import string
from datetime import datetime, timedelta
from certidude import authority, config, mailer, const
from certidude.relational import RelationalMixin
from certidude.common import random
class TokenManager(RelationalMixin):
SQL_CREATE_TABLES = "token_tables.sql"
def consume(self, uuid):
now = datetime.utcnow()
retval = self.get(
"select subject, mail, created, expires, profile from token where uuid = ? and created < ? and ? < expires and used is null",
uuid,
now + const.CLOCK_SKEW_TOLERANCE,
now - const.CLOCK_SKEW_TOLERANCE)
self.execute(
"update token set used = ? where uuid = ?",
now,
uuid)
return retval
def issue(self, issuer, subject, subject_mail=None):
# Expand variables
subject_username = subject.name
if not subject_mail:
subject_mail = subject.mail
# Generate token
token = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(32))
token_created = datetime.utcnow()
token_expires = token_created + config.TOKEN_LIFETIME
self.sql_execute("token_issue.sql",
token_created, token_expires, token,
issuer.name if issuer else None,
subject_username, subject_mail, "rw")
# Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata
try:
with open("/etc/timezone") as fh:
token_timezone = fh.read().strip()
except EnvironmentError:
token_timezone = None
router = sorted([j[0] for j in authority.list_signed(
common_name=config.SERVICE_ROUTERS)])[0]
protocols = ",".join(config.SERVICE_PROTOCOLS)
url = config.TOKEN_URL % locals()
context = globals()
context.update(locals())
mailer.send("token.md", to=subject_mail, **context)
return {
"token": token,
"url": url,
}
def list(self, expired=False, used=False, token=False):
stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject"
if token:
stmt += ", uuid"
stmt += " from token"
where = []
args = []
if not expired:
where.append(" expires > ?")
args.append(datetime.utcnow())
if not used:
where.append(" used is null")
if where:
stmt = stmt + " where " + (" and ".join(where))
stmt += " order by expires"
return self.iterfetch(stmt, *args)
def purge(self, all=False):
stmt = "delete from token"
args = []
if not all:
stmt += " where expires < ?"
args.append(datetime.utcnow())
return self.execute(stmt, *args)

View File

@ -22,6 +22,8 @@ class User(object):
return hash(self.mail) return hash(self.mail)
def __eq__(self, other): def __eq__(self, other):
if other == None:
return False
assert isinstance(other, User), "%s is not instance of User" % repr(other) assert isinstance(other, User), "%s is not instance of User" % repr(other)
return self.mail == other.mail return self.mail == other.mail
@ -90,7 +92,7 @@ class ActiveDirectoryUserManager(object):
# TODO: Sanitize username # TODO: Sanitize username
with DirectoryConnection() as conn: with DirectoryConnection() as conn:
ft = config.LDAP_USER_FILTER % username ft = config.LDAP_USER_FILTER % username
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName" attribs = "cn", "givenName", "sn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
for dn, entry in r: for dn, entry in r:
if not dn: if not dn:
@ -105,21 +107,21 @@ class ActiveDirectoryUserManager(object):
else: else:
given_name, surname = cn, b"" given_name, surname = cn, b""
mail, = entry.get("mail") or entry.get("userPrincipalName") or ((username + "@" + const.DOMAIN).encode("ascii"),) mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or ((username + "@" + const.DOMAIN).encode("ascii"),)
return User(username, mail.decode("ascii"), return User(username, mail.decode("ascii"),
given_name.decode("utf-8"), surname.decode("utf-8")) given_name.decode("utf-8"), surname.decode("utf-8"))
raise User.DoesNotExist("User %s does not exist" % username) raise User.DoesNotExist("User %s does not exist" % username)
def filter(self, ft): def filter(self, ft):
with DirectoryConnection() as conn: with DirectoryConnection() as conn:
attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName" attribs = "givenName", "surname", "samaccountname", "cn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
for dn,entry in r: for dn,entry in r:
if not dn: if not dn:
continue continue
username, = entry.get("sAMAccountName") username, = entry.get("sAMAccountName")
cn, = entry.get("cn") cn, = entry.get("cn")
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),) mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),)
if entry.get("givenName") and entry.get("sn"): if entry.get("givenName") and entry.get("sn"):
given_name, = entry.get("givenName") given_name, = entry.get("givenName")
surname, = entry.get("sn") surname, = entry.get("sn")

View File

@ -111,7 +111,7 @@ EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \ 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 netdata -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" -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"

View File

@ -29,9 +29,7 @@ AUTHORITY=$(hostname -f)
mkdir -p $OVERLAY/etc/config mkdir -p $OVERLAY/etc/config
mkdir -p $OVERLAY/etc/uci-defaults mkdir -p $OVERLAY/etc/uci-defaults
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/ mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/ cp /var/lib/certidude/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
cat <<EOF > $OVERLAY/etc/config/certidude cat <<EOF > $OVERLAY/etc/config/certidude

View File

@ -40,5 +40,7 @@ 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 usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun" -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun \
netdata"

View File

@ -103,8 +103,9 @@ uci set uhttpd.main.listen_http=0.0.0.0:8080
EOF EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \ iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun netdata \
strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm bc" -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm \
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"

View File

@ -1,6 +1,11 @@
# Randomize restart time
OFFSET=$(awk -v min=1 -v max=59 'BEGIN{srand(); print int(min+rand()*(max-min+1))}')
# wtf?! https://wiki.strongswan.org/issues/1501#note-7
cat << EOF > /etc/crontabs/root cat << EOF > /etc/crontabs/root
15 1 * * * sleep 70 && touch /etc/banner && reboot #$OFFSET 2 * * * sleep 70 && touch /etc/banner && reboot
10 1 1 */2 * /usr/bin/certidude-enroll-renew $OFFSET 2 * * * ipsec restart
5 1 1 */2 * /usr/bin/certidude-enroll-renew
EOF EOF
chmod 0600 /etc/crontabs/root chmod 0600 /etc/crontabs/root

View File

@ -122,6 +122,11 @@ logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)"
uci commit uci commit
echo $AUTHORITY_PATH >> /etc/sysupgrade.conf
echo $CERTIFICATE_PATH >> /etc/sysupgrade.conf
echo $KEY_PATH >> /etc/sysupgrade.conf
echo $REQUEST_PATH >> /etc/sysupgrade.conf
mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
# Start services # Start services

View File

@ -8,6 +8,8 @@ CERTIFICATE_PATH=$DIR/host_cert.pem
REQUEST_PATH=$DIR/host_req.pem REQUEST_PATH=$DIR/host_req.pem
KEY_PATH=$DIR/host_key.pem KEY_PATH=$DIR/host_key.pem
# TODO: fix Accepted 202 here
curl -f -L \ curl -f -L \
-H "Content-Type: application/pkcs10" \ -H "Content-Type: application/pkcs10" \
--data-binary @$REQUEST_PATH \ --data-binary @$REQUEST_PATH \

View File

@ -1,6 +1,8 @@
import pwd import pwd
from asn1crypto import pem, x509
from oscrypto import asymmetric from oscrypto import asymmetric
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from asn1crypto.util import OrderedDict
from subprocess import check_output from subprocess import check_output
from importlib import reload from importlib import reload
from click.testing import CliRunner from click.testing import CliRunner
@ -86,6 +88,9 @@ def clean_client():
def clean_server(): def clean_server():
# Stop Samba
os.system("systemctl stop samba-ad-dc")
os.umask(0o22) os.umask(0o22)
if os.path.exists("/run/certidude/server.pid"): if os.path.exists("/run/certidude/server.pid"):
@ -134,14 +139,9 @@ def clean_server():
if os.path.exists("/etc/openvpn/keys"): if os.path.exists("/etc/openvpn/keys"):
shutil.rmtree("/etc/openvpn/keys") shutil.rmtree("/etc/openvpn/keys")
# Stop samba # Remove Samba stuff
if os.path.exists("/run/samba/samba.pid"):
with open("/run/samba/samba.pid") as fh:
try:
os.kill(int(fh.read()), 15)
except OSError:
pass
os.system("rm -Rfv /var/lib/samba/*") os.system("rm -Rfv /var/lib/samba/*")
assert not os.path.exists("/var/lib/samba/private/secrets.keytab")
# 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")
@ -156,7 +156,7 @@ 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 samba krb5-user winbind bc") os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user")
assert_cleanliness() assert_cleanliness()
@ -211,10 +211,19 @@ 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 again with:
# - ECDSA certificates
# - POSIX auth
# - OCSP enabled
# - SCEP disabled
# - CRL enabled
assert os.system("certidude setup authority --elliptic-curve") == 0 assert os.system("certidude setup authority --elliptic-curve") == 0
assert_cleanliness() assert_cleanliness()
assert os.path.exists("/var/lib/certidude/signed/ca.example.lan.pem"), "provisioning failed"
assert not os.path.exists("/etc/cron.hourly/certidude")
# 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"
@ -222,12 +231,6 @@ def test_cli_setup_authority():
# Make sure we generated legit CA certificate # Make sure we generated legit CA certificate
from certidude import config, authority, user from certidude import config, authority, user
assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000
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_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert authority.public_key.algorithm == "ec"
# Generate garbage # Generate garbage
with open("/var/lib/certidude/bla", "w") as fh: with open("/var/lib/certidude/bla", "w") as fh:
@ -240,7 +243,6 @@ def test_cli_setup_authority():
pass pass
# Start server before any signing operations are performed # Start server before any signing operations are performed
config.CERTIFICATE_RENEWAL_ALLOWED = True
assert_cleanliness() assert_cleanliness()
import requests import requests
@ -255,16 +257,40 @@ def test_cli_setup_authority():
# Test CA certificate fetch # Test CA certificate fetch
buf = open("/var/lib/certidude/ca_cert.pem").read()
r = requests.get("http://ca.example.lan/api/certificate") r = requests.get("http://ca.example.lan/api/certificate")
assert r.status_code == 200 assert r.status_code == 200
assert r.headers.get('content-type') == "application/x-x509-ca-cert" assert r.headers.get('content-type') == "application/x-x509-ca-cert"
assert r.text == buf header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii"))
cert = x509.Certificate.load(certificate_der_bytes)
assert cert.subject.native.get("common_name") == "Certidude at ca.example.lan"
assert cert.subject.native.get("organizational_unit_name") == "Certificate Authority"
assert cert.serial_number >= 0x150000000000000000000000000000
assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
extensions = cert["tbs_certificate"]["extensions"].native
assert extensions[0] == OrderedDict([
('extn_id', 'basic_constraints'),
('critical', True),
('extn_value', OrderedDict([
('ca', True),
('path_len_constraint', None)]
))]), extensions[0]
# assert extensions[1][0] == "key_identifier", extensions[1]
assert extensions[2] == OrderedDict([
('extn_id', 'key_usage'),
('critical', True),
('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3]
assert len(extensions) == 3
public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"])
assert public_key.algorithm == "ec"
r = client().simulate_get("/api/certificate")
assert r.status_code == 200
assert r.headers.get('content-type') == "application/x-x509-ca-cert"
assert r.text == buf
# Password is bot, users created by Travis # Password is bot, users created by Travis
usertoken = "Basic dXNlcmJvdDpib3Q=" usertoken = "Basic dXNlcmJvdDpib3Q="
@ -419,6 +445,38 @@ def test_cli_setup_authority():
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/x-pem-file" assert r.headers.get('content-type') == "application/x-pem-file"
header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii"))
cert = x509.Certificate.load(certificate_der_bytes)
assert cert.subject.native.get("common_name") == "test"
assert cert.subject.native.get("organizational_unit_name") == "Roadwarrior"
assert cert.serial_number >= 0x150000000000000000000000000000
assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=100)
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"])
assert public_key.algorithm == "ec"
"""
extensions = cert["tbs_certificate"]["extensions"].native
assert extensions[0] == OrderedDict([
('extn_id', 'basic_constraints'),
('critical', True),
('extn_value', OrderedDict([
('ca', True),
('path_len_constraint', None)]
))]), extensions[0]
# assert extensions[1][0] == "key_identifier", extensions[1]
assert extensions[2] == OrderedDict([
('extn_id', 'key_usage'),
('critical', True),
('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3]
assert len(extensions) == 3
"""
r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"}) r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/json" assert r.headers.get('content-type') == "application/json"
@ -628,12 +686,7 @@ def test_cli_setup_authority():
assert ev_url.startswith("http://ca.example.lan/ev/sub/") assert ev_url.startswith("http://ca.example.lan/ev/sub/")
####################### # TODO: issue token, should fail because there are no routers
### Token mechanism ###
#######################
# TODO
############# #############
### nginx ### ### nginx ###
@ -768,6 +821,19 @@ def test_cli_setup_authority():
assert "Writing certificate to:" in result.output, result.output assert "Writing certificate to:" in result.output, result.output
#######################
### Token mechanism ###
#######################
r = client().simulate_post("/api/token/",
body="username=userbot",
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r.status_code == 200
# TODO: check consume
################################# #################################
### Subscribe to event source ### ### Subscribe to event source ###
################################# #################################
@ -1055,8 +1121,9 @@ def test_cli_setup_authority():
clean_server() clean_server()
# Bootstrap domain controller here, # Bootstrap domain controller here,
# Samba startup takes some timec # Samba startup takes some time
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca") os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
os.system("systemctl restart samba-ad-dc")
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'") os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'") os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
os.system("samba-tool group addmembers 'Domain Admins' adminbot") os.system("samba-tool group addmembers 'Domain Admins' adminbot")
@ -1069,7 +1136,7 @@ def test_cli_setup_authority():
with open("/etc/resolv.conf", "w") as fh: with open("/etc/resolv.conf", "w") as fh:
fh.write("nameserver 127.0.0.1\nsearch example.lan\n") fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
# TODO: dig -t srv perhaps? # TODO: dig -t srv perhaps?
os.system("samba")
# Samba bind 636 late (probably generating keypair) # Samba bind 636 late (probably generating keypair)
# so LDAPS connections below will fail # so LDAPS connections below will fail
@ -1088,28 +1155,29 @@ def test_cli_setup_authority():
assert os.system("echo S4l4k4l4 | kinit administrator") == 0 assert os.system("echo S4l4k4l4 | kinit administrator") == 0
assert os.path.exists("/tmp/krb5cc_0") assert os.path.exists("/tmp/krb5cc_0")
# Fork to not contaminate environment while creating service principal # Set up HTTP service principal
spn_pid = os.fork() os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ")
if not spn_pid: assert os.system("KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k") == 0
os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ") assert os.path.exists("/etc/certidude/server.keytab")
os.environ["KRB5_KTNAME"] = "FILE:/etc/certidude/server.keytab" os.system("chown root:certidude /etc/certidude/server.keytab")
assert os.system("net ads keytab add HTTP -k") == 0 os.system("chmod 640 /etc/certidude/server.keytab")
assert os.path.exists("/etc/certidude/server.keytab")
os.system("chown root:certidude /etc/certidude/server.keytab")
os.system("chmod 640 /etc/certidude/server.keytab")
return
else:
os.waitpid(spn_pid, 0)
assert_cleanliness() 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 again with:
# - RSA certificates
# - Kerberos auth
# - OCSP disabled
# - SCEP enabled
# - CRL disabled
# Bootstrap authority
assert not os.path.exists("/var/lib/certidude/ca_key.pem") assert not os.path.exists("/var/lib/certidude/ca_key.pem")
assert os.system("certidude setup authority --skip-packages") == 0 assert os.system("certidude setup authority --skip-packages") == 0
assert os.path.exists("/var/lib/certidude/ca_key.pem")
assert os.path.exists("/etc/cron.hourly/certidude")
# Make modifications to /etc/certidude/server.conf so # Make modifications to /etc/certidude/server.conf so
@ -1289,12 +1357,11 @@ def test_cli_setup_authority():
assert os.system("systemctl stop certidude") == 0 assert os.system("systemctl stop certidude") == 0
# 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() == \
"/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \ "/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
"/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \ "/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
"/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n" "/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n"
assert len(inbox) == 0, inbox # Make sure all messages were checked # TODO: pop mails from /var/mail and check content
os.system("service nginx stop") os.system("service nginx stop")
os.system("service openvpn stop") os.system("service openvpn stop")