1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 08:15:18 +00:00

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
---------------
To set up certificate authority in an isolated environment use a
vanilla Ubuntu 16.04 or container to collect the artifacts:
To prepare packages for offline installation use following snippet on a
vanilla Ubuntu 16.04 or container:
.. 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
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
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 .
tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
tar -cf certidude-server.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
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
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=[]):
from certidude import authority, config
from certidude.tokens import TokenManager
from .signed import SignedCertificateDetailResource
from .request import RequestListResource, RequestDetailResource
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/request/{cn}/", RequestDetailResource(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
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.
app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))

View File

@ -11,4 +11,5 @@ class LogResource(RelationalMixin):
@authorize_admin
def on_get(self, req, resp):
# 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 import ocsp
from base64 import b64decode
from certidude import config
from datetime import datetime
from certidude import config, const
from datetime import datetime, timedelta
from oscrypto import asymmetric
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
@ -88,7 +88,8 @@ class OCSPResource(AuthorityHandler):
'serial_number': serial,
},
'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': []
})

View File

@ -10,6 +10,7 @@ from base64 import b64decode
from certidude import config, push, errors
from certidude.decorators import csrf_protection, MyEncoder
from certidude.profile import SignatureProfile
from certidude.user import DirectoryConnection
from datetime import datetime
from oscrypto import asymmetric
from oscrypto.errors import SignatureError
@ -84,13 +85,28 @@ class RequestListResource(AuthorityHandler):
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
hit = False
with DirectoryConnection() as conn:
ft = config.LDAP_COMPUTER_FILTER % ("%s$" % machine)
attribs = "cn",
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
for dn, entry in r:
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 from %s",
machine, req.context.get("remote_addr"))
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.append_header("Content-Type", "application/x-x509-ca-cert")
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
const.HOSTNAME.encode("ascii"))
const.HOSTNAME)
class SessionResource(AuthorityHandler):
def __init__(self, authority, token_manager):
AuthorityHandler.__init__(self, authority)
self.token_manager = token_manager
@csrf_protection
@serialize
@login_required
@ -97,7 +101,7 @@ class SessionResource(AuthorityHandler):
signer_username = None
# TODO: dedup
yield dict(
serialized = dict(
serial = "%x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"),
common_name = common_name,
@ -109,12 +113,37 @@ class SessionResource(AuthorityHandler):
lease = lease,
tags = tags,
attributes = attributes or None,
extensions = dict([
(e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"]
if e["extn_id"].native in ("extended_key_usage",)])
responder_url = None
)
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" % (
req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent")))
return dict(
@ -130,10 +159,12 @@ class SessionResource(AuthorityHandler):
routers = [j[0] for j in self.authority.list_signed(
common_name=config.SERVICE_ROUTERS)]
),
authority = dict(
builder = dict(
profiles = config.IMAGE_BUILDER_PROFILES
profiles = config.IMAGE_BUILDER_PROFILES or None
),
authority = dict(
hostname = const.FQDN,
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],
lease = dict(
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),
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
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(
name = config.MAILER_NAME,
address = config.MAILER_ADDRESS
) if config.MAILER_ADDRESS else None,
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
requests=serialize_requests(self.authority.list_requests),
signed=serialize_certificates(self.authority.list_signed),
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(
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
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(
ocsp=bool(config.OCSP_SUBNETS),
crl=bool(config.CRL_SUBNETS),
token=bool(config.TOKEN_URL),
tagging=True,
leases=True,

View File

@ -1,11 +1,16 @@
import click
import codecs
import falcon
import logging
import hashlib
import os
import string
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from datetime import datetime
from datetime import datetime, timedelta
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.user import User
from certidude import config
@ -15,33 +20,25 @@ from .utils.firewall import login_required, authorize_admin
logger = logging.getLogger(__name__)
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):
# Consume token
now = time()
timestamp = req.get_param_as_int("t", required=True)
username = req.get_param("u", required=True)
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
try:
username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True))
except RelationalMixin.DoesNotExist:
raise falcon.HTTPForbidden("Forbidden", "No such token or token expired")
body = req.stream.read(req.content_length)
header, _, der_bytes = pem.unarmor(body)
csr = CertificationRequest.load(der_bytes)
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
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)
resp.set_header("Content-Type", "application/x-pem-file")
logger.info("Autosigned %s as proven by token ownership", common_name)
@ -56,40 +53,7 @@ class TokenResource(AuthorityHandler):
@login_required
@authorize_admin
def on_post(self, req, resp):
# Generate token
issuer = req.context.get("user")
username = req.get_param("username")
secondary = 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,
}
self.manager.issue(
issuer = req.context.get("user"),
subject = User.objects.get(req.get_param("username", required=True)),
subject_mail = req.get_param("mail"))

View File

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

View File

@ -13,26 +13,14 @@ from asn1crypto.csr import CertificationRequest
from certbuilder import CertificateBuilder
from certidude import config, push, mailer, const
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 csrbuilder import CSRBuilder, pem_armor_csr
from datetime import datetime, timedelta
from jinja2 import Template
from random import SystemRandom
from xattr import getxattr, listxattr, setxattr
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://jamielinux.com/docs/openssl-certificate-authority/
@ -61,13 +49,14 @@ def self_enroll(skip_notify=False):
self_public_key = asymmetric.load_public_key(path)
private_key = asymmetric.load_private_key(config.SELF_KEY_PATH)
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:
if public_key.algorithm == "ec":
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
elif public_key.algorithm == "rsa":
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
else:
NotImplemented
raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm)
fh.write(asymmetric.dump_private_key(private_key, None))
else:
now = datetime.utcnow()
@ -84,10 +73,11 @@ def self_enroll(skip_notify=False):
drop_privileges()
assert os.getuid() != 0 and os.getgid() != 0
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:
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"])
click.echo("Frontend certificate signed")
sys.exit(0)
else:
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()
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.issuer = certificate
builder.ca = profile.ca
builder.key_usage = profile.key_usage
builder.extended_key_usage = profile.extended_key_usage
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_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 csrbuilder import CSRBuilder, pem_armor_csr
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 glob import glob
from ipaddress import ip_network
@ -51,7 +51,7 @@ def setup_client(prefix="client_", dh=False):
authority = arguments.get("authority")
b = os.path.join("/etc/certidude/authority", authority)
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):
rpm("openssl")
apt("openssl")
@ -390,7 +390,6 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
}
if renew: # Do mutually authenticated TLS handshake
request_url = "https://%s:8443/api/request/" % authority_name
kwargs["cert"] = certificate_path, key_path
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
else:
@ -417,8 +416,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
else:
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:
request_url = request_url + "?" + "&".join(request_params)
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", "cert", certificate_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"))
nm_config.set("vpn", "cipher", "AES-128-GCM")
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",
type=click.File(mode="w", atomic=True, lazy=True),
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("--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")
@ -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("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
@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 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 \
rsync attr wget unzip"
click.echo("Running: %s" % cmd)
if os.system(cmd): sys.exit(254)
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253)
if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252)
if os.system(cmd):
raise click.ClickException("Failed to install APT packages")
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"):
click.echo("Enabling nginx PPA")
if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251)
if os.system("apt-get update -q"): sys.exit(250)
if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249)
if os.system("add-apt-repository -y ppa:nginx/stable"):
raise click.ClickException("Failed to add nginx PPA")
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:
click.echo("PPA for nginx already enabled")
if not os.path.exists("/usr/sbin/nginx"):
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:
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")
# 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")
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
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
assets_dir = os.path.join(directory, "assets")
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")
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)
dhparam_path = "/etc/ssl/dhparam.pem"
# Builder variables
dhgroup = "ecp384" if elliptic_curve else "modp2048"
@ -1080,8 +1095,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
except KeyError:
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
if subprocess.call(cmd):
click.echo("Failed to create system user 'certidude'")
return 255
raise click.ClickException("Failed to create system user 'certidude'")
if os.path.exists(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_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
letsencrypt = os.path.exists(letsencrypt_fullchain)
doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
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:
click.echo("Not attempting to install packages from NPM as requested...")
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)
if os.system(cmd): sys.exit(230)
if skip_assets:
click.echo("Not attempting to assemble assets as requested...")
else:
# Copy 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
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)
if os.system(cmd): sys.exit(228)
if os.system(cmd):
raise click.ClickException("Failed to compile nunjucks templates")
# Assemble bundle.js
click.echo("Assembling %s" % bundle_js)
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)):
click.echo("- Merging: %s" % j)
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):
click.echo("Creating %s" % 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
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):
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
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()))
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
if os.path.exists(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()))
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
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)
if not os.path.exists(path):
click.echo("Creating directory %s" % path)
os.mkdir(path)
else:
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
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()
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
builder = CertificateBuilder(distinguished_name, public_key)
builder.self_signed = True
builder.ca = True
builder.serial_number = random.randint(
0x100000000000000000000000000000000000000,
0xfffffffffffffffffffffffffffffffffffffff)
builder.serial_number = generate_serial()
builder.begin_date = NOW - timedelta(minutes=5)
builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE
builder.end_date = NOW + timedelta(days=authority_lifetime)
certificate = builder.build(private_key)
@ -1312,14 +1343,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
os.umask(0o137)
with open(ca_cert, 'wb') as f:
f.write(pem_armor_certificate(certificate))
click.echo("Authority certificate written to: %s" % ca_cert)
sys.exit(0) # stop this fork here
else:
_, exitcode = os.waitpid(bootstrap_pid, 0)
if exitcode:
return 0
from certidude import authority
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.stat(sqlite_path).st_mode == 0o100660
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 rsa -check -in %s" % ca_key)
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
@ -1461,7 +1501,7 @@ def certidude_revoke(common_name, reason):
@click.command("expire", help="Move expired certificates")
def certidude_expire():
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():
if expires < threshold:
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
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:")
for profile in config.PROFILES.values():
click.echo("- %s" % profile)
@ -1625,6 +1669,35 @@ def certidude_test(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")
def certidude_setup_strongswan(): pass
@ -1635,6 +1708,9 @@ def certidude_setup_openvpn(): pass
@click.group("setup", help="Getting started section")
def certidude_setup(): pass
@click.group("token", help="Token management")
def certidude_token(): pass
@click.group()
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_nginx)
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_serve)
entry_point.add_command(certidude_enroll)

View File

@ -2,6 +2,16 @@
import os
import click
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(
common_name="CN",
@ -122,3 +132,6 @@ def pip(packages):
pip.main(['install'] + packages.split(" "))
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_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
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
cp.get("authorization", "user subnets").split(" ") if j])
@ -71,8 +72,7 @@ USER_MULTIPLE_CERTIFICATES = {
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url")
AUTHORITY_CRL_URL = cp.get("signature", "revoked url")
AUTHORITY_OCSP_URL = cp.get("signature", "responder url")
AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN
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")
LDAP_USER_FILTER = cp.get("authorization", "ldap user 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_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
TOKEN_URL = cp.get("token", "url")
TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to seconds
TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
TOKEN_BACKEND = cp.get("token", "backend")
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
# 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, "extended key usage"),
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.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

View File

@ -3,17 +3,21 @@ import click
import os
import socket
import sys
from datetime import timedelta
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
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_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance
RUN_DIR = "/run/certidude"
CONFIG_DIR = "/etc/certidude"
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.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")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.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
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.title = title
self.ou = ou or None
@ -12,6 +12,9 @@ class SignatureProfile(object):
self.lifetime = lifetime
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.responder_url = responder_url
self.revoked_url = revoked_url
if common_name.startswith("^"):
self.common_name = common_name
elif common_name == "RE_HOSTNAME":
@ -39,7 +42,7 @@ class SignatureProfile(object):
def serialize(self):
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):
bits = []
@ -47,6 +50,10 @@ class SignatureProfile(object):
bits.append("%d years" % (self.lifetime / 365))
if 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)" % (
self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name)
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,
repr(self.common_name),
repr(self.responder_url),
repr(self.revoked_url))

View File

@ -14,6 +14,9 @@ class RelationalMixin(object):
SQL_CREATE_TABLES = ""
class DoesNotExist(Exception):
pass
def __init__(self, uri):
self.uri = urlparse(uri)
@ -29,7 +32,8 @@ class RelationalMixin(object):
if self.uri.netloc:
raise ValueError("Malformed database URI %s" % self.uri)
import sqlite3
conn = sqlite3.connect(self.uri.path)
conn = sqlite3.connect(self.uri.path,
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
else:
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()
return rowid
def iterfetch(self, query, *args):
conn = self.sql_connect()
cursor = conn.cursor()
@ -86,3 +89,24 @@ class RelationalMixin(object):
cursor.close()
conn.close()
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 (
id integer primary key autoincrement,
created datetime,
facility varchar(30),
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">
<ul class="navbar-nav mr-auto">
<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 class="nav-item">
<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">
<li class="nav-item hidden-xl-up">
<a class="nav-link" href="#">Log</a>
</li>
</ul>
<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>
</div>
</nav>

View File

@ -1,15 +1,8 @@
'use strict';
const KEYWORDS = [
["Android", "android"],
["iPhone", "iphone"],
["iPad", "ipad"],
["Ubuntu", "ubuntu"],
["Fedora", "fedora"],
["Linux", "linux"],
["Macintosh", "mac"],
];
const KEY_SIZE = 2048;
const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"];
jQuery.timeago.settings.allowFuture = true;
@ -17,38 +10,217 @@ function normalizeCommonName(j) {
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() {
var query = {};
window.query = {};
var a = location.hash.substring(1).split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');
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);
if (window.location.protocol != "https:") {
$.get("/api/certificate/", function(blob) {
$("#view-dashboard").html(env.render('views/insecure.html', { window: window,
authority_name: window.location.hostname,
session: { authority: { certificate: { blob: blob }}}
session: { authority: {
hostname: window.location.hostname,
certificate: { blob: blob }}}
}));
});
} else {
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) {
var cn = $(tag).attr("data-cn");
var id = $(tag).attr("title");
var value = $(tag).html();
function onTagClicked(e) {
e.preventDefault();
var cn = $(e.target).attr("data-cn");
var id = $(e.target).attr("title");
var value = $(e.target).html();
var updated = prompt("Enter new tag or clear to remove the tag", value);
if (updated == "") {
$(event.target).addClass("disabled");
@ -57,7 +229,7 @@ function onTagClicked(tag) {
url: "/api/signed/" + cn + "/tag/" + id + "/"
});
} else if (updated && updated != value) {
$(tag).addClass("disabled");
$(e.target).addClass("disabled");
$.ajax({
method: "PUT",
url: "/api/signed/" + cn + "/tag/" + id + "/",
@ -77,9 +249,10 @@ function onTagClicked(tag) {
return false;
}
function onNewTagClicked(menu) {
var cn = $(menu).attr("data-cn");
var key = $(menu).attr("data-key");
function onNewTagClicked(e) {
e.preventDefault();
var cn = $(e.target).attr("data-cn");
var key = $(e.target).attr("data-key");
var value = prompt("Enter new " + key + " tag for " + cn);
if (!value) return;
if (value.length == 0) return;
@ -101,6 +274,7 @@ function onNewTagClicked(menu) {
alert(e);
}
});
return false;
}
function onTagFilterChanged() {
@ -121,6 +295,7 @@ function onLogEntry (e) {
message: e.message,
severity: e.severity,
fresh: e.fresh,
keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|")
}
}));
}
@ -270,7 +445,7 @@ function onServerStopped() {
}
function onSendToken() {
function onIssueToken() {
$.ajax({
method: "POST",
url: "/api/token/",
@ -316,20 +491,16 @@ function loadAuthority(query) {
console.info("Loaded:", session);
$("#login").hide();
if (!query.columns) {
query.columns = 2;
}
$("#search").show();
/**
* Render authority views
**/
$("#view-dashboard").html(env.render('views/authority.html', {
session: session,
window: window,
columns: query.columns,
column_width: 12 / query.columns,
authority_name: window.location.hostname }));
window: window
}));
$("time").timeago();
if (session.authority) {
$("#log input").each(function(i, e) {
@ -414,12 +585,8 @@ function loadAuthority(query) {
$("#search").on("keyup", function() {
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
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) {
$("#request_submit").click(function() {
$(this).addClass("busy");
@ -442,11 +609,9 @@ function loadAuthority(query) {
alert(e);
}
});
});
}
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
$("#column-requests").show();
$("#column-signed").show();
@ -458,31 +623,36 @@ function loadAuthority(query) {
* Fetch log entries
*/
if (session.features.logging) {
if (query.columns == 4) {
if ($("#column-log:visible").length) {
loadLog();
} else {
}
$("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() {
if (window.log_initialized) return;
if (window.log_initialized) {
console.info("Log already loaded");
return;
}
console.info("Loading log...");
window.log_initialized = true;
$.ajax({
method: "GET",
url: "/api/log/",
url: "/api/log/?limit=100",
dataType: "json",
success: function(entries, status, xhr) {
console.info("Got", entries.length, "log entries");
console.info("j=", entries.length-1);
for (var j = entries.length-1; j--; ) {
onLogEntry(entries[j]);
};

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/
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
common name = $HOSTNAME
system wide = true
EOF
cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf
{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
[IPSec to {{ router }}]
authority = {{ authority_name }}
authority = {{ session.authority.hostname }}
service = network-manager/strongswan
remote = {{ router }}
{% endif %}{% if "openvpn" in session.service.protocols %}
[OpenVPN to {{ router }}]
authority = {{ authority_name }}
authority = {{ session.authority.hostname }}
service = network-manager/openvpn
remote = {{ router }}
{% endif %}{% endfor %}
EOF
{% endif %}{% endfor %}EOF
certidude enroll

View File

@ -1,8 +1,8 @@
# 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
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
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
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 dnf && dnf install openvpn
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
client
nobind
{% for router in session.service.routers %}
remote {{ router }} 1194 udp
remote {{ router }} 443 tcp-client
{% endfor %}
tls-version-min 1.2
tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384
cipher AES-128-GCM
auth SHA384
mute-replay-warnings
reneg-sec 0
remote-cert-tls server
dev tun
persist-tun
persist-key
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
# Create OpenVPN configuration file
cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF
{% include "snippets/openvpn-client.conf" %}
EOF
# Restart OpenVPN service
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
uci set openvpn.$section.script_security=2
uci set openvpn.$section.client_connect='/etc/certidude/updown'
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
uci set openvpn.$section.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_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 %}
uci set openvpn.$section.enabled=1

View File

@ -1,7 +1,7 @@
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
--key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
--cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
'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 /bin/hostname && NAME=$(hostname)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
{% include "snippets/update-trust.sh" %}
{% include "snippets/request-common.sh" %}
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
# Submit CSR and save signed certificate
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
'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 \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
# Delete CA certificate if checksum doesn't match
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" %}
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 \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
-out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
-key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
-out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem
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 /bin/hostname && NAME=$(hostname -f)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
{% include "snippets/update-trust.sh" %}
{% include "snippets/request-common.sh" %}
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
# Submit CSR and save signed certificate
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes'

View File

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

View File

@ -1,10 +1,10 @@
cat > /etc/ipsec.conf << EOF
config setup
strictcrlpolicy=yes
ca {{ authority_name }}
ca {{ session.authority.hostname }}
auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem
conn client-to-site
auto=start
@ -12,7 +12,7 @@ conn client-to-site
rightsubnet=0.0.0.0/0
rightca="{{ session.authority.certificate.distinguished_name }}"
left=%defaultroute
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
leftsourceip=%config
leftca="{{ session.authority.certificate.distinguished_name }}"
keyexchange=ikev2
@ -21,9 +21,8 @@ conn client-to-site
closeaction=restart
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
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
# Set SELinux context
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
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/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.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
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
/etc/certidude/authority/**
/etc/certidude/authority/** r,
EOF
systemctl restart

View File

@ -4,19 +4,17 @@ config setup
strictcrlpolicy=yes
uniqueids=yes
ca {{ authority_name }}
ca {{ session.authority.hostname }}
auto=add
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem
conn default-{{ authority_name }}
conn default-{{ 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 %}!
left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftid={{ session.service.routers | first }}
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown
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
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
leftca="{{ session.authority.certificate.distinguished_name }}"
@ -27,15 +25,15 @@ conn default-{{ authority_name }}
conn site-to-clients
auto=add
also=default-{{ authority_name }}
also=default-{{ session.authority.hostname }}
conn site-to-client1
auto=ignore
also=default-{{ authority_name }}
also=default-{{ session.authority.hostname }}
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
rightsourceip=172.21.0.1
EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
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 \
&& 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
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
@"
{{ session.authority.certificate.blob }}
"@ | Out-File ca_cert.pem
{{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem
{% if session.authority.certificate.algorithm == "ec" %}
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
{% else %}
@ -25,25 +24,25 @@ KeyAlgorithm = ECDSA_P384
KeyLength = 2048
{% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
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
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
{% endif %}
# Set up IPSec VPN tunnel
Remove-VpnConnection -AllUserConnection -Force k-space
{% for router in session.service.routers %}
# Set up IPSec VPN tunnel to {{ router }}
Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}"
Add-VpnConnection `
-Name k-space `
-ServerAddress guests.k-space.ee `
-Name "IPSec to {{ router }}" `
-ServerAddress {{ router }} `
-AuthenticationMethod MachineCertificate `
-SplitTunneling `
-TunnelType ikev2 `
-PassThru -AllUserConnection
# Security hardening
Set-VpnConnectionIPsecConfiguration `
-ConnectionName k-space `
-ConnectionName "IPSec to {{ router }}" `
-AuthenticationTransformConstants GCMAES128 `
-CipherTransformConstants GCMAES128 `
-EncryptionMethod AES256 `
@ -51,6 +50,8 @@ Set-VpnConnectionIPsecConfiguration `
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
-PassThru -AllUserConnection -Force
{% endfor %}
{#
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

View File

@ -5,21 +5,71 @@
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Request submission</h4>
</div>
<form action="/api/request/" method="post">
<div class="modal-body">
<h5>Certidude client</h5>
<ul class="nav nav-pills" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a>
</li>
<li class="nav-item">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a>
</li>
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a>
</li>
{% if "openvpn" in session.service.protocols %}
<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>
</li>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<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 %}
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
<p>On Windows execute following PowerShell script</p>
<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">
<h5>UNIX & UNIX-like</h5>
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
<div class="highlight">
@ -32,24 +82,20 @@
</div>
<p>To renew:</p>
<div class="highlight">
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
</div>
</div>
{% if "openvpn" in session.service.protocols %}
<h5>OpenVPN as client</h5>
<!-- 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>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<h5>StrongSwan as client</h5>
</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>Then install software:</p>
@ -59,19 +105,26 @@
<p>To configure StrongSwan as roadwarrior:</p>
<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>
<p>First enroll certificates using the snippet from UNIX section above</p>
<!-- 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>Then:</p>
<div class="highlight">
<pre class="code"><code>opkg install curl libmbedtls
# Derive FQDN from WAN interface's reverse DNS record
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
# Derive FQDN from WAN interface's reverse DNS record
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" %}
</code></pre>
</div>
@ -87,33 +140,21 @@
<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>
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>
{% 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>
<!-- Copy & paste -->
<div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste">
<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>
<form action="/api/request/" method="post">
<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>
@ -123,6 +164,10 @@
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="revocation_list_modal" role="dialog">
@ -133,10 +178,10 @@
<h4 class="modal-title">Revocation lists</h4>
</div>
<div class="modal-body">
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
<p>To fetch <a href="http://{{ session.authority.hostname }}/api/revoked/">certificate revocation list</a>:</p>
<pre><code>curl http://{{ session.authority.hostname }}/api/revoked/ > crl.der
curl http://{{ session.authority.hostname }}/api/revoked/ -L -H "Accept: application/x-pem-file"
curl http://{{ session.authority.hostname }}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
</div>
<div class="modal-footer">
<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 class="row">
<div class="col-sm-{{ column_width }}">
<div class="col-sm-6 col-lg-4 col-xl-3">
<h1>Signed certificates</h1>
<p>Authority administration allowed for
{% for user in session.authority.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authority.admin_subnets %}anywhere{% else %}
{% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
<p>Authority administration
{% if session.authority.certificate.organization %}of {{ session.authority.certificate.organization }}{% 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>.
Following certificates have been signed:</p>
<div id="signed_certificates">
@ -159,7 +210,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
{% endfor %}
</div>
</div>
<div class="col-sm-{{ column_width }}">
<div class="col-sm-6 col-lg-4 col-xl-3">
{% if session.authority %}
{% if session.features.token %}
<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_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2">
<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>
</div>
</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>
{% endif %}
{% if session.authorization.request_subnets %}
<h1>Pending requests</h1>
<p>Use Certidude client to apply for a certificate.
{% if not session.authority.request_subnets %}
{% if not session.authorization.request_subnets %}
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.
{% else %}
Request submission allowed from
{% for subnet in session.authority.request_subnets %}
{% for subnet in session.authorization.request_subnets %}
{{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %}.
{% endif %}
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 "0.0.0.0/0" in session.authority.autosign_subnets %}
{% if session.authorization.autosign_subnets %}
{% if "0.0.0.0/0" in session.authorization.autosign_subnets %}
All requests are automatically signed.
{% else %}
Requests from
{% for subnet in session.authority.autosign_subnets %}
{% 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 %}
{% for subnet in session.authorization.scep_subnets %}
{{ subnet }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% 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>
{% if columns >= 3 %}
</div>
<div class="col-sm-{{ column_width }}">
{% endif %}
{% if session.builder.profiles %}
<h2>LEDE imagebuilder</h2>
<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.builder.profiles %}
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="col-sm-6 col-lg-4 col-xl-3">
<h1>Revoked certificates</h1>
<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>
@ -227,7 +316,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
{% include "views/revoked.html" %}
{% endfor %}
</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"></div>
<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;">
<h1>Log</h1>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked> Critical</label>
<label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label>
<label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label>
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</label>
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off"> Debug</label>
<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-error" type="checkbox" autocomplete="off" checked>Error</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"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label>
</div>
<ul id="log-entries" class="list-group">
</ul>
<p>Click here to load more entries</p>
</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.
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.
</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>
<i class="fa fa-{{ entry.severity }}-circle"/>
{{ entry.message }}

View File

@ -40,8 +40,8 @@
<div class="collapse" id="details-{{ request.sha256sum }}">
<p>Use following to fetch the signing request:</p>
<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>
curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/ \
<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://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \
| openssl req -text -noout</code></pre>
</div>

View File

@ -29,15 +29,15 @@
<p>To fetch certificate:</p>
<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>
curl http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/ \
<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://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \
| openssl x509 -text -noout</code></pre>
</div>
<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 \
-url http://{{ window.location.hostname }}/api/ocsp/ \
-url http://{{ session.authority.hostname }}/api/ocsp/ \
-serial 0x{{ certificate.serial }}</span></code></pre>
<p>

View File

@ -56,7 +56,7 @@
<div class="btn-group">
{% 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>
<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>
@ -64,7 +64,7 @@
<div class="dropdown-menu">
{% for tag_category in session.authority.tagging %}
<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 %}
</div>
{% endif %}
@ -74,24 +74,29 @@
<p>To fetch certificate:</p>
<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>
curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/ \
<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 --cert-status http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/ \
| openssl x509 -text -noout</code></pre>
</div>
{% if session.features.ocsp %}
<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
{% if session.authorization.ocsp_subnets %}
{% if certificate.responder_url %}
<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 \
-url http://{{ window.location.hostname }}/api/ocsp/ \
-url {{ certificate.responder_url }} \
-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 %}
<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/ \
--cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \
--key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \
--cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code>
<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/{{ session.authority.hostname }}/ca_cert.pem \
--key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
--cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem</pre></code>
<div style="overflow: auto; max-width: 100%;">
<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>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
{% if certificate.extensions.extended_key_usage %}
<tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr>
{% if certificate.key_usage %}
<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 %}
</tbody>
</table>

View File

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

View File

@ -1,9 +1,9 @@
Token for {{ user.name }}
Token for {{ subject }}
{% if issuer == user %}
Token has been issued for {{ user }} for retrieving profile from link below.
{% if issuer == subject %}
Token has been issued for {{ subject }} for retrieving profile from link below.
{% else %}
{{ issuer }} has provided {{ user }} a token for retrieving
{{ issuer }} has provided {{ subject }} a token for retrieving
profile from the link below.
{% 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
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
#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
map $ssl_client_s_dn $ssl_client_s_dn_cn {

View File

@ -1,13 +1,16 @@
[DEFAULT]
# LEDE image builder profiles enabled by default
enabled = yes
# Path to filesystem overlay used
overlay = {{ doc_path }}/overlay
# 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
# use it to include SSH keys, set passwords, etc
script =
script = /etc/certidude/script/site.sh
# Which subnets are routed to the tunnel
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 }}!
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
command = {{ doc_path }}/builder/ap.sh
[tpl-wdr3600-factory]
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
model = archer-c7-v2
model = tl-wdr3600-v1
# 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
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
[cf-e380ac]
title = Comfast E380AC (Access Point)
[cf-e380ac-factory]
enabled = no
title = Comfast E380AC (Access Point), TFTP-friendly
command = {{ doc_path }}/builder/ap.sh
model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.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)
command = {{ doc_path }}/builder/mfp.sh
model = gl-ar150
filename = ar71xx-generic-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)
command = {{ doc_path }}/builder/ipcam.sh
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
limit_conn addr 10;
client_body_timeout 5s;
@ -72,6 +66,9 @@ server {
# Uncomment following to enable HTTPS
#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 {
@ -110,7 +107,7 @@ server {
alias /var/www/html/.well-known/;
}
{% if not push_server %}
{% if not push_server %}
# Event stream for pushing events to web browsers
location ~ "^/ev/sub/(.*)" {
nchan_channel_id $1;
@ -122,14 +119,17 @@ server {
nchan_channel_id $1;
nchan_subscriber longpoll;
}
{% endif %}
{% endif %}
access_log /var/log/nginx/certidude-frontend-access.log;
error_log /var/log/nginx/certidude-frontend-error.log;
}
server {
# Section for certificate authenticated HTTPS clients,
# for submitting information to CA eg. leases,
# renewing certificates and
# requesting/renewing certificates and
# for delivering scripts to clients
server_name {{ common_name }};
@ -150,6 +150,9 @@ server {
nchan_channel_id $1;
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 %}
@ -167,6 +170,10 @@ server {
nchan_publisher;
nchan_channel_id $1;
}
access_log /var/log/nginx/certidude-push-access.log;
error_log /var/log/nginx/certidude-push-error.log;
}
{% endif %}

View File

@ -1,4 +1,5 @@
[DEFAULT]
enabled = no
ou =
lifetime = 120
ca = false
@ -6,7 +7,19 @@ common name = RE_COMMON_NAME
key usage = digital_signature key_encipherment
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]
enabled = yes
title = Certificate Authority
common name = ^ca
ca = true
@ -15,12 +28,14 @@ extended key usage =
lifetime = 1095
[rw]
enabled = yes
title = Roadwarrior
ou = Roadwarrior
common name = RE_HOSTNAME
extended key usage = client_auth
[srv]
enabled = yes
title = Server
ou = Server
common name = RE_FQDN
@ -28,6 +43,7 @@ lifetime = 120
extended key usage = server_auth client_auth
[gw]
enabled = yes
title = Gateway
ou = Gateway
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
[ap]
enabled = no
title = Access Point
ou = Access Point
common name = RE_HOSTNAME
@ -43,6 +60,7 @@ lifetime = 120
extended key usage = client_auth
[mfp]
enabled = no
title = Printers
ou = MFP
common name = ^mfp\-
@ -50,9 +68,16 @@ lifetime = 120
extended key usage = client_auth
[cam]
enabled = no
title = Camera
ou = IP Camera
common name = ^cam\-
lifetime = 120
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
{% endif %}
ldap mail attribute = mail
;ldap mail attribute = otherMailbox
[authorization]
# The authorization backend specifies how the users are authorized.
# 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
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]
# This should occasionally be regenerated
@ -242,8 +235,12 @@ expired dir = {{ directory }}/expired/
# and make sure Certidude machine doesn't try to accept mails.
# uncomment mail sender address to enable e-mails.
# Make sure used e-mail address is reachable for end users.
name = Certificate management
address = certificates@example.lan
name = Certidude at {{ common_name }}
{% if domain %}
address = certificates@{{ domain }}
{% else %}
address = certificates@exaple.com
{% endif %}
[tagging]
owner/string = Owner
@ -259,17 +256,20 @@ services template = {{ template_path }}/bootstrap.conf
[token]
# 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.
# Token mechanism disabled by setting URL setting to none
;url = http://ca.example.com/
url =
# Backend for tokens, set none to disable
;backend =
backend = sql
# 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.
lifetime = 30
lifetime = 2880
# Secret for generating and validating tokens, regenerate occasionally
secret = {{ token_secret }}
[script]
# 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]
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)
def __eq__(self, other):
if other == None:
return False
assert isinstance(other, User), "%s is not instance of User" % repr(other)
return self.mail == other.mail
@ -90,7 +92,7 @@ class ActiveDirectoryUserManager(object):
# TODO: Sanitize username
with DirectoryConnection() as conn:
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)
for dn, entry in r:
if not dn:
@ -105,21 +107,21 @@ class ActiveDirectoryUserManager(object):
else:
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"),
given_name.decode("utf-8"), surname.decode("utf-8"))
raise User.DoesNotExist("User %s does not exist" % username)
def filter(self, ft):
with DirectoryConnection() as conn:
attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName"
attribs = "givenName", "surname", "samaccountname", "cn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
for dn,entry in r:
if not dn:
continue
username, = entry.get("sAMAccountName")
cn, = entry.get("cn")
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + 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"):
given_name, = entry.get("givenName")
surname, = entry.get("sn")

View File

@ -111,7 +111,7 @@ EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \
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 \
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
htop iftop netdata -odhcp6c -odhcpd -dnsmasq \
-luci-app-firewall \
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-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/uci-defaults
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
cp /var/lib/certidude/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
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 \
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 \
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun"
iftop tcpdump nmap nano usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
-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
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 \
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
15 1 * * * sleep 70 && touch /etc/banner && reboot
10 1 1 */2 * /usr/bin/certidude-enroll-renew
#$OFFSET 2 * * * sleep 70 && touch /etc/banner && reboot
$OFFSET 2 * * * ipsec restart
5 1 1 */2 * /usr/bin/certidude-enroll-renew
EOF
chmod 0600 /etc/crontabs/root

View File

@ -122,6 +122,11 @@ logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)"
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
# Start services

View File

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

View File

@ -1,6 +1,8 @@
import pwd
from asn1crypto import pem, x509
from oscrypto import asymmetric
from csrbuilder import CSRBuilder, pem_armor_csr
from asn1crypto.util import OrderedDict
from subprocess import check_output
from importlib import reload
from click.testing import CliRunner
@ -86,6 +88,9 @@ def clean_client():
def clean_server():
# Stop Samba
os.system("systemctl stop samba-ad-dc")
os.umask(0o22)
if os.path.exists("/run/certidude/server.pid"):
@ -134,14 +139,9 @@ def clean_server():
if os.path.exists("/etc/openvpn/keys"):
shutil.rmtree("/etc/openvpn/keys")
# Stop samba
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
# Remove Samba stuff
os.system("rm -Rfv /var/lib/samba/*")
assert not os.path.exists("/var/lib/samba/private/secrets.keytab")
# Restore initial 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 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()
@ -211,10 +211,19 @@ def test_cli_setup_authority():
assert const.HOSTNAME == "ca"
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_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
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
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
with open("/var/lib/certidude/bla", "w") as fh:
@ -240,7 +243,6 @@ def test_cli_setup_authority():
pass
# Start server before any signing operations are performed
config.CERTIFICATE_RENEWAL_ALLOWED = True
assert_cleanliness()
import requests
@ -255,16 +257,40 @@ def test_cli_setup_authority():
# Test CA certificate fetch
buf = open("/var/lib/certidude/ca_cert.pem").read()
r = requests.get("http://ca.example.lan/api/certificate")
assert r.status_code == 200
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
usertoken = "Basic dXNlcmJvdDpib3Q="
@ -419,6 +445,38 @@ def test_cli_setup_authority():
assert r.status_code == 200, r.text
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"})
assert r.status_code == 200, r.text
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/")
#######################
### Token mechanism ###
#######################
# TODO
# TODO: issue token, should fail because there are no routers
#############
### nginx ###
@ -768,6 +821,19 @@ def test_cli_setup_authority():
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 ###
#################################
@ -1055,8 +1121,9 @@ def test_cli_setup_authority():
clean_server()
# 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("systemctl restart samba-ad-dc")
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 group addmembers 'Domain Admins' adminbot")
@ -1069,7 +1136,7 @@ def test_cli_setup_authority():
with open("/etc/resolv.conf", "w") as fh:
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
# TODO: dig -t srv perhaps?
os.system("samba")
# Samba bind 636 late (probably generating keypair)
# 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.path.exists("/tmp/krb5cc_0")
# Fork to not contaminate environment while creating service principal
spn_pid = os.fork()
if not spn_pid:
# Set up HTTP service principal
os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ")
os.environ["KRB5_KTNAME"] = "FILE:/etc/certidude/server.keytab"
assert os.system("net ads keytab add HTTP -k") == 0
assert os.system("KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k") == 0
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()
r = requests.get("http://ca.example.lan/api/")
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 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
@ -1289,12 +1357,11 @@ def test_cli_setup_authority():
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() == \
"/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/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 openvpn stop")