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
18
README.rst
@ -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
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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"))
|
||||
|
@ -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': []
|
||||
})
|
||||
|
||||
|
@ -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))
|
||||
|
||||
# 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"))
|
||||
return
|
||||
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 (%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))
|
||||
|
||||
|
||||
"""
|
||||
|
@ -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)]
|
||||
),
|
||||
builder = dict(
|
||||
profiles = config.IMAGE_BUILDER_PROFILES or None
|
||||
),
|
||||
authority = dict(
|
||||
builder = dict(
|
||||
profiles = config.IMAGE_BUILDER_PROFILES
|
||||
),
|
||||
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,
|
||||
|
@ -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"))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
148
certidude/cli.py
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,5 @@
|
||||
create table if not exists log (
|
||||
id integer primary key autoincrement,
|
||||
created datetime,
|
||||
facility varchar(30),
|
||||
level int,
|
||||
|
17
certidude/sql/sqlite/token_issue.sql
Normal file
@ -0,0 +1,17 @@
|
||||
insert into token (
|
||||
created,
|
||||
expires,
|
||||
uuid,
|
||||
issuer,
|
||||
subject,
|
||||
mail,
|
||||
profile
|
||||
) values (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
);
|
13
certidude/sql/sqlite/token_tables.sql
Normal 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)
|
||||
)
|
BIN
certidude/static/img/ubuntu-01-edit-connections.png
Normal file
After Width: | Height: | Size: 393 KiB |
BIN
certidude/static/img/ubuntu-02-network-connections.png
Normal file
After Width: | Height: | Size: 368 KiB |
BIN
certidude/static/img/ubuntu-03-import-saved-config.png
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
certidude/static/img/ubuntu-04-select-file.png
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
certidude/static/img/ubuntu-05-profile-imported.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
certidude/static/img/ubuntu-06-ipv4-settings.png
Normal file
After Width: | Height: | Size: 318 KiB |
BIN
certidude/static/img/ubuntu-07-disable-default-route.png
Normal file
After Width: | Height: | Size: 342 KiB |
BIN
certidude/static/img/ubuntu-08-activate-connection.png
Normal file
After Width: | Height: | Size: 394 KiB |
BIN
certidude/static/img/windows-01-download-openvpn.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
certidude/static/img/windows-02-install-openvpn.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
certidude/static/img/windows-03-move-config-file.png
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
certidude/static/img/windows-04-connect.png
Normal file
After Width: | Height: | Size: 583 KiB |
BIN
certidude/static/img/windows-05-connected.png
Normal file
After Width: | Height: | Size: 638 KiB |
@ -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>
|
||||
|
@ -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 {
|
||||
loadAuthority(query);
|
||||
if (query.action == "enroll") {
|
||||
$("#view-dashboard").html(env.render('views/enroll.html'));
|
||||
var options = document.querySelectorAll(".option");
|
||||
for (i = 0; i < options.length; i++) {
|
||||
options[i].style.display = "none";
|
||||
}
|
||||
setTimeout(onKeyGen, 100);
|
||||
console.info("Generating key pair...");
|
||||
} else {
|
||||
loadAuthority(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onTagClicked(tag) {
|
||||
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() {
|
||||
$("#column-requests").show();
|
||||
$("#column-signed").show();
|
||||
$("#column-revoked").show();
|
||||
$("#column-log").hide();
|
||||
});
|
||||
}
|
||||
$("nav .nav-link.log").removeClass("disabled").click(function() {
|
||||
loadLog();
|
||||
$("#column-requests").show();
|
||||
$("#column-signed").show();
|
||||
$("#column-revoked").show();
|
||||
$("#column-log").hide();
|
||||
});
|
||||
} else {
|
||||
console.info("Log disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadLog() {
|
||||
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]);
|
||||
};
|
||||
|
15
certidude/static/snippets/ansible-site.yml
Normal 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 %}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
97
certidude/static/snippets/ios.mobileconfig
Normal 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>
|
||||
|
35
certidude/static/snippets/openvpn-client.conf
Normal 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
|
@ -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
|
||||
|
||||
#}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -5,51 +5,97 @@
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Request submission</h4>
|
||||
</div>
|
||||
<form action="/api/request/" method="post">
|
||||
<div class="modal-body">
|
||||
<h5>Certidude client</h5>
|
||||
<p>On Ubuntu or Fedora:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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>
|
||||
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
||||
<h5>UNIX & UNIX-like</h5>
|
||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||
<div class="highlight">
|
||||
|
||||
<pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/request-server.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>To renew:</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
|
||||
</div>
|
||||
<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 %}
|
||||
<h5>OpenVPN as client</h5>
|
||||
|
||||
<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>
|
||||
<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 %}
|
||||
<h5>StrongSwan as client</h5>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-ansible" role="tab" aria-controls="ansible" aria-selected="false">Ansible</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a>
|
||||
</li>
|
||||
|
||||
{% if session.authorization.scep_subnets %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-scep" role="tab" aria-controls="scep" aria-selected="false">SCEP</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<!-- Certidude client -->
|
||||
<div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude">
|
||||
<p>On Ubuntu or Fedora:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Windows -->
|
||||
<div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows">
|
||||
<p>On Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %} execute following PowerShell script</p>
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- UNIX-like -->
|
||||
<div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix">
|
||||
|
||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||
<div class="highlight">
|
||||
|
||||
<pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/request-server.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>To renew:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenVPN as client -->
|
||||
<div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn">
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
<p>Then install software:</p>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<!-- StrongSwan as client -->
|
||||
<div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan">
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
|
||||
<p>Then install software:</p>
|
||||
@ -59,68 +105,67 @@
|
||||
|
||||
<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>
|
||||
<!-- LEDE -->
|
||||
<div class="tab-pane fade" id="snippet-lede" role="tabpanel" aria-labelledby="lede">
|
||||
<p>To enroll from OpenWrt/LEDE and to set it up as OpenVPN/IKEv2 gateway,
|
||||
first enroll certificates using the snippet from UNIX section above</p>
|
||||
|
||||
<p>First enroll certificates using the snippet from UNIX section above</p>
|
||||
|
||||
<p>Then:</p>
|
||||
<div class="highlight">
|
||||
<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
|
||||
<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
|
||||
{% include "snippets/gateway-updown.sh" %}
|
||||
</code></pre>
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<p>Then either set up OpenVPN service:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
|
||||
{% include "snippets/strongswan-server.sh" %}
|
||||
ipsec restart</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<p>Then either set up OpenVPN service:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- 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>
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
|
||||
{% include "snippets/strongswan-server.sh" %}
|
||||
ipsec restart</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.authority.builder %}
|
||||
<h5>OpenWrt/LEDE image builder</h5>
|
||||
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
|
||||
<ul>
|
||||
{% for name, title, filename in session.authority.builder.profiles %}
|
||||
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h5>SCEP</h5>
|
||||
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
|
||||
|
||||
<h5>Copy & paste</h5>
|
||||
|
||||
<p>Use whatever tools you have available on your platform to generate
|
||||
keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
|
||||
|
||||
<textarea id="request_body" style="width:100%; min-height: 10em;"
|
||||
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||
<form action="/api/request/" method="post">
|
||||
<textarea id="request_body" style="width:100%; min-height: 10em;"
|
||||
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 %}
|
||||
|
||||
<h1>Pending requests</h1>
|
||||
{% if session.authorization.request_subnets %}
|
||||
<h1>Pending requests</h1>
|
||||
|
||||
<p>Use Certidude client to apply for a certificate.
|
||||
<p>Use Certidude client to apply for a certificate.
|
||||
|
||||
{% if not session.authority.request_subnets %}
|
||||
Request submission disabled.
|
||||
{% elif "0.0.0.0/0" in session.authority.request_subnets %}
|
||||
Request submission is enabled.
|
||||
{% else %}
|
||||
Request submission allowed from
|
||||
{% for subnet in session.authority.request_subnets %}
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}.
|
||||
{% endif %}
|
||||
{% if not session.authorization.request_subnets %}
|
||||
Request submission disabled.
|
||||
{% elif "0.0.0.0/0" in session.authorization.request_subnets %}
|
||||
Request submission is enabled.
|
||||
{% else %}
|
||||
Request submission allowed from
|
||||
{% 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.
|
||||
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 %}
|
||||
All requests are automatically signed.
|
||||
{% 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.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 %}
|
||||
Requests from
|
||||
{% for subnet in session.authority.autosign_subnets %}
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
are automatically signed.
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
|
||||
{% 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-{{ column_width }}">
|
||||
{% endif %}
|
||||
<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>
|
||||
|
238
certidude/static/views/enroll.html
Normal 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>
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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*\.
|
||||
|
34
certidude/templates/server/site.sh
Normal 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
@ -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)
|
@ -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")
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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:
|
||||
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.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)
|
||||
# Set up HTTP service principal
|
||||
os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ")
|
||||
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")
|
||||
|
||||
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")
|
||||
|