Integrate LEDE image builder

This commit is contained in:
Lauri Võsandi 2018-01-03 22:12:02 +00:00
parent 345c2802ea
commit fba8f5d776
18 changed files with 386 additions and 62 deletions

View File

@ -82,7 +82,7 @@ class SessionResource(object):
attributes = {} attributes = {}
for key in listxattr(path): for key in listxattr(path):
if key.startswith(b"user.machine."): if key.startswith(b"user.machine."):
attributes[key[13:]] = getxattr(path, key).decode("ascii") attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii")
# Extract lease information from filesystem # Extract lease information from filesystem
try: try:
@ -131,6 +131,9 @@ class SessionResource(object):
), ),
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
authority = dict( authority = dict(
builder = dict(
profiles = config.IMAGE_BUILDER_PROFILES
),
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
lease = dict( lease = dict(
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
@ -208,6 +211,7 @@ def certidude_app(log_handlers=[]):
from .attrib import AttributeResource from .attrib import AttributeResource
from .bootstrap import BootstrapResource from .bootstrap import BootstrapResource
from .token import TokenResource from .token import TokenResource
from .builder import ImageBuilderResource
app = falcon.API(middleware=NormalizeMiddleware()) app = falcon.API(middleware=NormalizeMiddleware())
app.req_options.auto_parse_form_urlencoded = True app.req_options.auto_parse_form_urlencoded = True
@ -240,6 +244,9 @@ def certidude_app(log_handlers=[]):
# Bootstrap resource # Bootstrap resource
app.add_route("/api/bootstrap/", BootstrapResource()) app.add_route("/api/bootstrap/", BootstrapResource())
# LEDE image builder resource
app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource())
# Add CRL handler if we have any whitelisted subnets # Add CRL handler if we have any whitelisted subnets
if config.CRL_SUBNETS: if config.CRL_SUBNETS:
from .revoked import RevocationListResource from .revoked import RevocationListResource

52
certidude/api/builder.py Normal file
View File

@ -0,0 +1,52 @@
import click
import falcon
import logging
import os
import subprocess
from certidude import config, const
from certidude.auth import login_required, authorize_admin
from jinja2 import Template
logger = logging.getLogger(__name__)
class ImageBuilderResource(object):
@login_required
@authorize_admin
def on_get(self, req, resp, profile, suggested_filename):
model = config.cp2.get(profile, "model")
build_script_path = config.cp2.get(profile, "command")
overlay_path = config.cp2.get(profile, "overlay")
site_script_path = config.cp2.get(profile, "script")
suffix = config.cp2.get(profile, "filename")
build = "/var/lib/certidude/builder/" + profile
if not os.path.exists(build + "/overlay/etc/uci-defaults"):
os.makedirs(build + "/overlay/etc/uci-defaults")
os.system("rsync -av " + overlay_path + "/ " + build + "/overlay/")
if site_script_path:
template = Template(open(site_script_path).read())
with open(build + "/overlay/etc/uci-defaults/99-site-config", "w") as fh:
fh.write(template.render(authority_name=const.FQDN))
proc = subprocess.Popen(("/bin/bash", build_script_path),
stdout=open(build + "/build.log", "w"), stderr=subprocess.STDOUT,
close_fds=True, shell=False,
cwd=build,
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin"},
startupinfo=None, creationflags=0)
proc.communicate()
for dname in os.listdir(build):
if dname.startswith("lede-imagebuilder-"):
for root, dirs, files in os.walk(os.path.join(build, dname, "bin", "targets")):
for filename in files:
if filename.endswith(suffix):
path = os.path.join(root, filename)
click.echo("Serving: %s" % path)
resp.body = open(path, "rb").read()
resp.set_header("Content-Disposition", ("attachment; filename=%s" % suggested_filename))
return
raise falcon.HTTPNotFound()

View File

@ -1,5 +1,6 @@
import falcon import falcon
import logging import logging
import os
from certidude import const, config, authority from certidude import const, config, authority
from certidude.decorators import serialize from certidude.decorators import serialize
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@ -26,9 +27,10 @@ class ScriptResource():
except AttributeError: # No tags except AttributeError: # No tags
pass pass
script = named_tags.get("script", config.SCRIPT_DEFAULT) script = named_tags.get("script", "default.sh")
assert script in os.listdir(config.SCRIPT_DIR)
resp.set_header("Content-Type", "text/x-shellscript") resp.set_header("Content-Type", "text/x-shellscript")
resp.body = env.get_template(script).render( resp.body = env.get_template(os.path.join(script)).render(
authority_name=const.FQDN, authority_name=const.FQDN,
common_name=cn, common_name=cn,
other_tags=other_tags, other_tags=other_tags,

View File

@ -96,7 +96,7 @@ def setup_client(prefix="client_", dh=False):
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll") @click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign") @click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
if not skip_self and os.path.exists(const.CONFIG_PATH): if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Self-enrolling authority's web interface certificate") click.echo("Self-enrolling authority's web interface certificate")
from certidude import authority from certidude import authority
authority.self_enroll() authority.self_enroll()
@ -944,38 +944,42 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--directory", help="Directory for authority files") @click.option("--directory", help="Directory for authority files")
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags") @click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN) @click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@fqdn_required @fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title): def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages):
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
import pwd import pwd
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
click.echo("Installing packages...") if skip_packages:
os.system("apt-get install -qq -y cython3 python3-dev python3-mimeparse \ click.echo("Not attempting to install packages from APT as requested...")
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
libkrb5-dev libldap2-dev libsasl2-dev")
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
os.system("pip3 install -q --pre --upgrade python-ldap")
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
click.echo("Enabling nginx PPA")
os.system("add-apt-repository -y ppa:nginx/stable")
os.system("apt-get update -q")
os.system("apt-get install -y -q libnginx-mod-nchan")
else: else:
click.echo("PPA for nginx already enabled") click.echo("Installing packages...")
os.system("apt-get install -qq -y cython3 python3-dev python3-mimeparse \
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev")
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
os.system("pip3 install -q --pre --upgrade python-ldap")
if not os.path.exists("/usr/sbin/nginx"): if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
click.echo("Installing nginx from PPA") click.echo("Enabling nginx PPA")
os.system("apt-get install -y -q nginx") os.system("add-apt-repository -y ppa:nginx/stable")
else: os.system("apt-get update -q")
click.echo("Web server nginx already installed") os.system("apt-get install -y -q libnginx-mod-nchan")
else:
click.echo("PPA for nginx already enabled")
if not os.path.exists("/usr/bin/node"): if not os.path.exists("/usr/sbin/nginx"):
os.symlink("/usr/bin/nodejs", "/usr/bin/node") click.echo("Installing nginx from PPA")
os.system("apt-get install -y -q nginx")
else:
click.echo("Web server nginx already installed")
if not os.path.exists("/usr/bin/node"):
os.symlink("/usr/bin/nodejs", "/usr/bin/node")
# Generate secret for tokens # Generate secret for tokens
token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50)) token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50))
@ -1036,6 +1040,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else: else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
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")
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
certidude_path = sys.argv[0] certidude_path = sys.argv[0]
@ -1057,6 +1064,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else: else:
click.echo("Not systemd based OS, don't know how to set up initscripts") click.echo("Not systemd based OS, don't know how to set up initscripts")
if os.path.exists("/etc/certidude/builder.conf"):
click.echo("Image builder config /etc/certidude/builder.conf already exists, remove to regenerate")
else:
with open("/etc/certidude/builder.conf", "w") as fh:
fh.write(env.get_template("server/builder.conf").render(vars()))
click.echo("File /etc/certidude/builder.conf created")
assert os.getuid() == 0 and os.getgid() == 0 assert os.getuid() == 0 and os.getgid() == 0
bootstrap_pid = os.fork() bootstrap_pid = os.fork()
if not bootstrap_pid: if not bootstrap_pid:
@ -1069,7 +1083,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
os.makedirs(subdir) os.makedirs(subdir)
# Install JavaScript pacakges # Install JavaScript pacakges
os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg") if skip_packages:
click.echo("Not attempting to install packages from NPM as requested...")
else:
os.system("npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg")
# Compile nunjucks templates # Compile nunjucks templates
cmd = 'nunjucks-precompile --include ".html$" --include ".svg" %s > %s.part' % (static_path, bundle_js) cmd = 'nunjucks-precompile --include ".html$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
@ -1109,14 +1126,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
if not os.path.exists(const.CONFIG_DIR): if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % const.CONFIG_DIR) click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR) os.makedirs(const.CONFIG_DIR)
if os.path.exists(const.CONFIG_PATH): if os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Configuration file %s already exists, remove to regenerate" % const.CONFIG_PATH) click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
else: else:
os.umask(0o137) os.umask(0o137)
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)]) push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
with open(const.CONFIG_PATH, "w") as fh: with open(const.SERVER_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/server.conf").render(vars())) fh.write(env.get_template("server/server.conf").render(vars()))
click.echo("Generated %s" % const.CONFIG_PATH) click.echo("Generated %s" % const.SERVER_CONFIG_PATH)
# Create directory with 755 permissions # Create directory with 755 permissions
os.umask(0o022) os.umask(0o022)
@ -1183,7 +1200,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
from certidude import authority from certidude import authority
authority.self_enroll() authority.self_enroll()
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.CONFIG_PATH) click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo() click.echo()
click.echo("Use following commands to inspect the newly created files:") click.echo("Use following commands to inspect the newly created files:")
click.echo() click.echo()
@ -1337,7 +1354,7 @@ def certidude_serve(port, listen, fork):
if port == 80: if port == 80:
click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!") click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")
click.echo("Using configuration from: %s" % const.CONFIG_PATH) click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH)
log_handlers = [] log_handlers = []

View File

@ -12,7 +12,7 @@ from random import choice
# Options that are parsed from config file are fetched here # Options that are parsed from config file are fetched here
cp = configparser.RawConfigParser() cp = configparser.RawConfigParser()
cp.readfp(codecs.open(const.CONFIG_PATH, "r", "utf8")) cp.readfp(open(const.SERVER_CONFIG_PATH, "r"))
AUTHENTICATION_BACKENDS = set([j for j in AUTHENTICATION_BACKENDS = set([j for j in
cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap
@ -99,7 +99,10 @@ TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
# TODO: Check if we don't have base or servers # TODO: Check if we don't have base or servers
# The API call for looking up scripts uses following directory as root # The API call for looking up scripts uses following directory as root
SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") SCRIPT_DIR = cp.get("script", "path")
SCRIPT_DEFAULT = "default.sh"
PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]] for i in cp.options("profile")]) PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]] for i in cp.options("profile")])
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()]

View File

@ -7,7 +7,8 @@ import sys
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
RUN_DIR = "/run/certidude" RUN_DIR = "/run/certidude"
CONFIG_DIR = "/etc/certidude" CONFIG_DIR = "/etc/certidude"
CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")

View File

@ -1,6 +1,8 @@
import falcon import falcon
import logging import logging
import click
from asn1crypto import pem, x509
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -50,6 +52,17 @@ def whitelist_subject(func):
except IOError: except IOError:
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
else: else:
# First attempt to authenticate client with certificate
buf = req.get_header("X-SSL-CERT")
if buf:
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
origin_cert = x509.Certificate.load(der_bytes)
if origin_cert.native == cert.native:
click.echo("Subject authenticated using certificates")
return func(self, req, resp, cn, *args, **kwargs)
# For backwards compatibility check source IP address
# TODO: make it disableable
try: try:
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii") inner_address = getxattr(path, "user.lease.inner_address").decode("ascii")
except IOError: except IOError:
@ -58,6 +71,6 @@ def whitelist_subject(func):
if req.context.get("remote_addr") != ip_address(inner_address): if req.context.get("remote_addr") != ip_address(inner_address):
raise falcon.HTTPForbidden("Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr")) raise falcon.HTTPForbidden("Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr"))
else: else:
return func(self, req, resp, cn, *args, **kwargs) return func(self, req, resp, cn, *args, **kwargs)
return wrapped return wrapped

View File

@ -1,3 +1,3 @@
{% for key, value in certificate.attributes %} {% for key, value in certificate.attributes %}
<span class="attribute icon {{ key | replace('.', ' ') }}" title="{{ key }}={{ value }}">{{ value }}</span> <span class="badge badge-info" title="{{ key }}={{ value }}">{{ value }}</span>
{% endfor %} {% endfor %}

View File

@ -28,7 +28,7 @@ curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre> http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre>
</div> </div>
<h5>OpenWrt/LEDE</h5> <h5>Vanilla OpenWrt/LEDE</h5>
<p>On OpenWrt/LEDE router to convert it into VPN gateway:</p> <p>On OpenWrt/LEDE router to convert it into VPN gateway:</p>
<div class="highlight"> <div class="highlight">
@ -45,6 +45,16 @@ curl -f -L -H "Content-type: application/pkcs10" \
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre> http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
</div> </div>
{% if session.authority.builder %}
<h5>OpenWrt/LEDE image builder</h5>
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
<ul>
{% for name, title, filename in session.authority.builder.profiles %}
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
{% endfor %}
</ul>
{% endif %}
<h5>SCEP</h5> <h5>SCEP</h5>
<p>Use following as the enrollment URL: http://{{ window.location.hostname }}/cgi-bin/pkiclient.exe</p> <p>Use following as the enrollment URL: http://{{ window.location.hostname }}/cgi-bin/pkiclient.exe</p>

View File

@ -24,11 +24,17 @@
Part of {{ certificate.organizational_unit }} organizational unit. Part of {{ certificate.organizational_unit }} organizational unit.
{% endif %} {% endif %}
</p> </p>
<p>
{% if session.authority.tagging %} {% if session.authority.tagging %}
<p class="tags" data-cn="{{ certificate.common_name }}"> <span class="tags" data-cn="{{ certificate.common_name }}">
{% include "views/tags.html" %} {% include "views/tags.html" %}
</p> </span>
{% endif %} {% endif %}
<span class="attributes" data-cn="{{ certificate.common_name }}">
{% include "views/attributes.html" %}
</span>
</p>
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button> <button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button>
<button type="button" class="btn btn-danger" <button type="button" class="btn btn-danger"
@ -46,25 +52,24 @@
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=9',type:'delete'});">Revoke due to withdrawn privilege</a> onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=9',type:'delete'});">Revoke due to withdrawn privilege</a>
</div> </div>
</div> </div>
<div class="collapse" id="details-{{ certificate.sha256sum }}">
<p>
<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 }}">
<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>
</button>
<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>
{% endfor %}
</div>
{% endif %}
</div>
</p>
<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 }}">
<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>
</button>
<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>
{% endfor %}
</div>
{% endif %}
</div>
<div class="collapse" id="details-{{ certificate.sha256sum }}">
<p>To fetch certificate:</p> <p>To fetch certificate:</p>
<div class="bd-example"> <div class="bd-example">
@ -77,7 +82,7 @@ curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name
<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://{{ window.location.hostname }}/api/certificate/ > session.pem
openssl ocsp -issuer session.pem -CAfile session.pem \ openssl ocsp -issuer session.pem -CAfile session.pem \
-url http://{{ window.location.hostname }}/api/ocsp/ \ -url http://{{ window.location.hostname }}/api/ocsp/ \
-serial 0x{{ certificate.serial }}</span></code></pre> -serial 0x{{ certificate.serial }}</code></pre>
<p>To fetch script:</p> <p>To fetch script:</p>
<pre><code class="language-bash" data-lang="bash">cd /var/lib/certidude/{{ window.location.hostname }}/ <pre><code class="language-bash" data-lang="bash">cd /var/lib/certidude/{{ window.location.hostname }}/

View File

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

View File

@ -0,0 +1,31 @@
[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 }}/build-ap.sh
# Path to filesystem overlay, used
overlay = {{ doc_path }}/overlay
# Site specific script to be copied to /etc/uci-defaults/99-site-script
script =
# Device/model/profile selection
model = archer-c7-v2
# File that will be picked from the bin/ folder
filename = archer-c7-v2-squashfs-factory-eu.bin
# And renamed to make it TFTP-friendly
rename = ArcherC7v2_tp_recovery.bin
[cf-e380ac]
title = Comfast E380AC (Access Point)
command = {{ doc_path }}/build-ap.sh
overlay = {{ doc_path }}/overlay
script =
model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin
rename = firmware_auto.bin

View File

@ -198,3 +198,9 @@ default = client, 120,
srv = server, 365, Server srv = server, 365, Server
gw = server, 3, Gateway gw = server, 3, Gateway
ap = client, 1825, Access Point ap = client, 1825, Access Point
[script]
# Path to the folder with scripts that can be served to the clients, set none to disable scripting
path = {{ script_dir }}
;path = /etc/certidude/script
;path =

58
doc/build-ap.sh Normal file
View File

@ -0,0 +1,58 @@
#!/bin/bash
set -e
set -x
umask 022
VERSION=17.01.4
BASENAME=lede-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64
FILENAME=$BASENAME.tar.xz
URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
PACKAGES="luci luci-app-commands \
collectd collectd-mod-conntrack collectd-mod-interface \
collectd-mod-iwinfo collectd-mod-load collectd-mod-memory \
collectd-mod-network collectd-mod-protocols collectd-mod-tcpconns \
collectd-mod-uptime \
openssl-util openvpn-openssl curl ca-certificates \
htop iftop tcpdump nmap nano -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"
if [ ! -e $FILENAME ]; then
wget -q $URL
fi
if [ ! -e $BASENAME ]; then
tar xf $FILENAME
fi
cd $BASENAME
# Copy CA certificate
AUTHORITY=$(hostname -f)
CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
if [ -d "$CERTIDUDE_DIR" ]; then
mkdir -p overlay/$CERTIDUDE_DIR
cp $CERTIDUDE_DIR/ca_cert.pem overlay/$CERTIDUDE_DIR
fi
cat < EOF > overlay/etc/config/certidude
config authority
option url http://$AUTHORITY
option authority_path /var/lib/certidude/$AUTHORITY/ca_cert.pem
option request_path /var/lib/certidude/$AUTHORITY/client_req.pem
option certificate_path /var/lib/certidude/$AUTHORITY/client_cert.pem
option key_path /var/lib/certidude/$AUTHORITY/client_key.pem
option key_type rsa
option key_length 1024
option red_led gl-connect:red:wlan
option green_led gl-connect:green:lan
EOF
make image FILES=../overlay/ PACKAGES="$PACKAGES" PROFILE="$PROFILE"

32
doc/overlay/etc/profile Normal file
View File

@ -0,0 +1,32 @@
#!/bin/sh
[ -f /etc/banner ] && cat /etc/banner
[ -e /tmp/.failsafe ] && cat /etc/banner.failsafe
export PATH=/usr/bin:/usr/sbin:/bin:/sbin
export HOME=$(grep -e "^${USER:-root}:" /etc/passwd | cut -d ":" -f 6)
export HOME=${HOME:-/root}
export PS1='\u@\h:\w\$ '
[ -z "$KSH_VERSION" -o \! -s /etc/mkshrc ] || . /etc/mkshrc
[ -x /bin/more ] || alias more=less
[ -x /usr/bin/vim ] && alias vi=vim || alias vim=vi
[ -x /usr/bin/arp ] || arp() { cat /proc/net/arp; }
[ -x /usr/bin/ldd ] || ldd() { LD_TRACE_LOADED_OBJECTS=1 $*; }
HOSTNAME=$(uci get system.@system[0].hostname)
DOMAIN=$(uci -q get dhcp.@dnsmasq[0].domain)
if [ $? -eq 0 ]; then
FQDN=$HOSTNAME.$DOMAIN
else
FQDN=$HOSTNAME
fi
export PS1='\[\033[01;31m\]$FQDN\[\033[01;34m\] \W #\[\033[00m\] '
case "$TERM" in
xterm*|rxvt*)
echo -ne "\033]0;${USER}@${FQDN}:${PWD}\007"
;;
*)
;;
esac

View File

@ -0,0 +1,21 @@
MODEL=$(cat /etc/board.json | jsonfilter -e '@["model"]["id"]')
# Hostname prefix
case $MODEL in
tl-*|archer-*) VENDOR=tplink ;;
cf-*) VENDOR=comfast ;;
*) VENDOR=ap ;;
esac
# Network interface with relevant MAC address
case $MODEL in
tl-wdr*) NIC=wlan1 ;;
archer-*) NIC=eth1 ;;
cf-e380ac-v2) NIC=eth0 ;;
*) NIC=wlan0 ;;
esac
HOSTNAME=$VENDOR-$(cat /sys/class/net/$NIC/address | cut -d : -f 4- | sed -e 's/://g')
uci set system.@system[0].hostname=$HOSTNAME
uci set network.lan.hostname=$HOSTNAME

View File

@ -0,0 +1,66 @@
# Disable DHCP servers
/etc/init.d/odhcpd disable
/etc/init.d/dnsmasq disable
# Remove firewall rules since AP bridges ethernet to wireless anyway
uci delete firewall.@zone[1]
uci delete firewall.@zone[0]
uci delete firewall.@forwarding[0]
for j in $(seq 0 10); do uci delete firewall.@rule[0]; done
# Remove WAN interface
uci delete network.wan
uci delete network.wan6
# Reconfigure DHCP client for bridge over LAN and WAN ports
uci delete network.lan.ipaddr
uci delete network.lan.netmask
uci delete network.lan.ip6assign
uci delete network.globals.ula_prefix
uci delete network.@switch_vlan[1]
uci delete dhcp.@dnsmasq[0].domain
uci set network.lan.proto=dhcp
uci set network.lan.ipv6=0
uci set network.lan.ifname='eth0'
uci set network.lan.stp=1
# Radio ordering differs among models
case $(uci get wireless.radio0.hwmode) in
11a) uci rename wireless.radio0=radio5ghz;;
11g) uci rename wireless.radio0=radio2ghz;;
esac
case $(uci get wireless.radio1.hwmode) in
11a) uci rename wireless.radio1=radio5ghz;;
11g) uci rename wireless.radio1=radio2ghz;;
esac
# Reset virtual SSID-s
uci delete wireless.@wifi-iface[1]
uci delete wireless.@wifi-iface[0]
# Pseudorandomize channel selection, should work with 80MHz on 5GHz band
case $(uci get system.@system[0].hostname | md5sum) in
1*|2*|3*|4*) uci set wireless.radio2ghz.channel=1; uci set wireless.radio5ghz.channel=36 ;;
5*|6*|7*|8*) uci set wireless.radio2ghz.channel=5; uci set wireless.radio5ghz.channel=52 ;;
9*|0*|a*|b*) uci set wireless.radio2ghz.channel=9; uci set wireless.radio5ghz.channel=100 ;;
c*|d*|e*|f*) uci set wireless.radio2ghz.channel=13; uci set wireless.radio5ghz.channel=132 ;;
esac
# Create bridge for guests
uci set network.guest=interface
uci set network.guest.proto='static'
uci set network.guest.address='0.0.0.0'
uci set network.guest.type='bridge'
uci set network.guest.ifname='eth0.156' # tag id 156 for guest network
uci set network.guest.ipaddr='0.0.0.0'
uci set network.guest.ipv6=0
uci set network.guest.stp=1
# Disable switch tagging and bridge all ports on TP-Link WDR3600/WDR4300
case $(cat /etc/board.json | jsonfilter -e '@["model"]["id"]') in
tl-wdr*)
uci set network.@switch[0].enable_vlan=0
uci set network.@switch_vlan[0].ports='0 1 2 3 4 5 6'
;;
*) ;;
esac