Bugfixes, OU support and image builder fixes

This commit is contained in:
Lauri Võsandi 2018-01-23 13:13:49 +00:00
parent 388f58574b
commit 5cb7f89c1b
13 changed files with 148 additions and 51 deletions

View File

@ -44,6 +44,7 @@ class SessionResource(object):
except IOError:
submission_hostname = None
yield dict(
server = authority.server_flags(common_name),
submitted = submitted,
common_name = common_name,
address = submission_address,
@ -103,6 +104,7 @@ class SessionResource(object):
yield dict(
serial = "%x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"),
common_name = common_name,
# TODO: key type, key length, key exponent, key modulo
signed = signed,
@ -158,10 +160,8 @@ class SessionResource(object):
request_subnets = config.REQUEST_SUBNETS or None,
admin_subnets=config.ADMIN_SUBNETS or None,
signature = dict(
server_certificate_lifetime=config.SERVER_CERTIFICATE_LIFETIME,
client_certificate_lifetime=config.CLIENT_CERTIFICATE_LIFETIME,
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
profiles = [dict(organizational_unit=ou, flags=f, lifetime=lt) for f, lt, ou in config.PROFILES.values()]
profiles = [dict(name=k, server=v[0]=="server", lifetime=v[1], organizational_unit=v[2], title=v[3]) for k,v in config.PROFILES.items()]
)
) if req.context.get("user").is_admin() else None,
features=dict(

View File

@ -21,6 +21,7 @@ class ImageBuilderResource(object):
suffix = config.cp2.get(profile, "filename")
build = "/var/lib/certidude/builder/" + profile
log_path = build + "/build.log"
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/")
@ -31,12 +32,16 @@ class ImageBuilderResource(object):
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,
stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
close_fds=True, shell=False,
cwd=build,
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin"},
cwd=os.path.dirname(os.path.realpath(build_script_path)),
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
"BUILD":build, "OVERLAY":build + "/overlay/"},
startupinfo=None, creationflags=0)
proc.communicate()
if proc.returncode:
logger.info("Build script finished with non-zero exitcode, see %s for more information" % log_path)
raise falcon.HTTPInternalServerError("Build script finished with non-zero exitcode")
for dname in os.listdir(build):
if dname.startswith("lede-imagebuilder-"):

View File

@ -33,6 +33,11 @@ class LeaseResource(object):
@authorize_server
def on_post(self, req, resp):
client_common_name = req.get_param("client", required=True)
if "=" in client_common_name: # It's actually DN, resolve it to CN
_, client_common_name = client_common_name.split(" CN=", 1)
if "," in client_common_name:
client_common_name, _ = client_common_name.split(",", 1)
path, buf, cert, signed, expires = authority.get_signed(client_common_name) # TODO: catch exceptions
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied")

View File

@ -225,7 +225,10 @@ class RequestDetailResource(object):
Sign a certificate signing request
"""
try:
cert, buf = authority.sign(cn, ou=req.get_param("ou"), overwrite=True, signer=req.context.get("user").name)
cert, buf = authority.sign(cn,
profile=req.get_param("profile", default="default"),
overwrite=True,
signer=req.context.get("user").name)
# Mailing and long poll publishing implemented in the function above
except EnvironmentError: # no such CSR
raise falcon.HTTPNotFound()

View File

@ -38,6 +38,7 @@ class SignedCertificateDetailResource(object):
common_name = cn,
signer = signer_username,
serial_number = "%x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"),
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
sha256sum = hashlib.sha256(buf).hexdigest()))

View File

@ -55,7 +55,7 @@ def self_enroll():
fh.write(asymmetric.dump_private_key(private_key, None))
else:
now = datetime.utcnow()
if now - timedelta(days=1) < expires:
if now + timedelta(days=1) < expires:
click.echo("Certificate %s still valid, delete to self-enroll again" % path)
return
@ -307,7 +307,7 @@ def delete_request(common_name):
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
headers={"User-Agent": "Certidude API"})
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, ou=None, signer=None):
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None):
"""
Sign certificate signing request by it's common name
"""
@ -320,13 +320,15 @@ def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, ou=No
# Sign with function below
cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, ou, signer)
cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer)
os.unlink(req_path)
return cert, buf
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None, signer=None):
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None):
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
if profile not in config.PROFILES:
raise ValueError("Invalid profile supplied '%s'" % profile)
assert buf.startswith(b"-----BEGIN CERTIFICATE REQUEST-----")
assert isinstance(csr, CertificationRequest)
@ -367,8 +369,9 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None
# Sign via signer process
dn = {u'common_name': common_name }
if ou:
dn["organizational_unit"] = ou
profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile]
lifetime = int(lifetime)
builder = CertificateBuilder(dn, csr_pubkey)
builder.serial_number = random.randint(
0x1000000000000000000000000000000000000000,
@ -376,16 +379,14 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None
now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5)
builder.end_date = now + timedelta(days=config.SERVER_CERTIFICATE_LIFETIME
if server_flags(common_name)
else config.CLIENT_CERTIFICATE_LIFETIME)
builder.end_date = now + timedelta(days=lifetime)
builder.issuer = certificate
builder.ca = False
builder.key_usage = set(["digital_signature", "key_encipherment"])
# OpenVPN uses CN while StrongSwan uses SAN
if server_flags(common_name):
builder.subject_alt_domains = [common_name]
# If we have FQDN and profile suggests server flags, enable them
if server_flags(common_name) and profile_server_flags:
builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server
builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"])
else:
builder.extended_key_usage = set(["client_auth"])

View File

@ -1312,7 +1312,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
def certidude_sign(common_name, overwrite):
from certidude import authority
drop_privileges()
cert = authority.sign(common_name, overwrite)
cert = authority.sign(common_name, overwrite=overwrite)
@click.command("revoke", help="Revoke certificate")

View File

@ -60,8 +60,6 @@ USER_MULTIPLE_CERTIFICATES = {
cp.get("authority", "user enrollment")]
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
CLIENT_CERTIFICATE_LIFETIME = cp.getint("signature", "client certificate lifetime")
SERVER_CERTIFICATE_LIFETIME = cp.getint("signature", "server certificate lifetime")
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")

View File

@ -118,7 +118,7 @@ function onRequestSubmitted(e) {
console.info("Going to prepend:", request);
onRequestDeleted(e); // Delete any existing ones just in case
$("#pending_requests").prepend(
env.render('views/request.html', { request: request }));
env.render('views/request.html', { request: request, session: session }));
$("#pending_requests time").timeago();
},
error: function(response) {

View File

@ -28,23 +28,105 @@ 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>
</div>
<h5>Vanilla OpenWrt/LEDE</h5>
<h5>OpenVPN gateway on OpenWrt/LEDE router</h5>
<p>On OpenWrt/LEDE router to convert it into VPN gateway:</p>
<p>First enroll certificates:</p>
<div class="highlight">
<pre class="code"><code>mkdir -p /var/lib/certidude/{{ window.location.hostname }}; \
grep -c certidude /etc/sysupgrade.conf || echo /var/lib/certidude >> /etc/sysupgrade.conf; \
curl -f http://{{ window.location.hostname }}/api/certificate/ -o /var/lib/certidude/{{ window.location.hostname }}/ca_cert.pem; \
test -e /var/lib/certidude/{{ window.location.hostname }}/client_key.pem || openssl genrsa -out /var/lib/certidude/{{ window.location.hostname }}/client_key.pem 2048; \
test -e /var/lib/certidude/{{ window.location.hostname }}/client_req.pem || read -p "Enter FQDN: " NAME; openssl req -new -sha256 \
-key /var/lib/certidude/{{ window.location.hostname }}/client_key.pem \
-out /var/lib/certidude/{{ window.location.hostname }}/client_req.pem -subj "/CN=$NAME"; \
<pre class="code"><code># 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)
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}; \
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf; \
curl -f http://{{ window.location.hostname }}/api/certificate/ -o /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem; \
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|| openssl genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048; \
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|| openssl req -new -sha256 \
-key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
-out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"; \
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/var/lib/certidude/{{ window.location.hostname }}/client_req.pem \
-o /var/lib/certidude/{{ window.location.hostname }}/client_cert.pem \
--data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
-o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
</div>
<p>Then set up service:</p>
<div class="highlight">
<pre class="code"><code># Create VPN gateway up/down script for reporting client IP addresses to CA
cat <<\EOF > /etc/certidude/updown
#!/bin/sh
CURL="curl -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/"
case $PLUTO_VERB in
up-client|down-client) $CURL --data "outer_address=$PLUTO_PEER&inner_address=$PLUTO_PEER_SOURCEIP&client=$PLUTO_PEER_ID" ;;
*) $CURL --data "client=$X509_0_CN&outer_address=$untrusted_ip&inner_address=$ifconfig_pool_remote_ip&serial=$tls_serial_0" ;;
esac
EOF
chmod +x /etc/certidude/updown
# Generate Diffie-Hellman parameters file for OpenVPN
test -e /etc/certidude/dh.pem \
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
# Create interface definition for tunnel
uci set network.vpn=interface
uci set network.vpn.name='vpn'
uci set network.vpn.ifname=tun_s2c
uci set network.vpn.proto='none'
# Create zone definition for VPN interface
uci set firewall.vpn=zone
uci set firewall.vpn.name='vpn'
uci set firewall.vpn.input='ACCEPT'
uci set firewall.vpn.forward='ACCEPT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.network='vpn'
# Allow UDP 1194 on WAN interface
uci set firewall.openvpn=rule
uci set firewall.openvpn.name='Allow OpenVPN'
uci set firewall.openvpn.src='wan'
uci set firewall.openvpn.dest_port=1194
uci set firewall.openvpn.proto='udp'
uci set firewall.openvpn.target='ACCEPT'
# Forward traffic from VPN to LAN
uci set firewall.c2s=forwarding
uci set firewall.c2s.src='vpn'
uci set firewall.c2s.dest='lan'
# Permit DNS queries from VPN
uci set dhcp.@dnsmasq[0].localservice='0'
touch /etc/config/openvpn
uci set openvpn.s2c=openvpn
uci set openvpn.s2c.local=$(uci get network.wan.ipaddr)
uci set openvpn.s2c.script_security=2
uci set openvpn.s2c.client_connect='/etc/certidude/updown'
uci set openvpn.s2c.tls_version_min='1.2'
uci set openvpn.s2c.tls_cipher='TLS-DHE-RSA-WITH-AES-256-GCM-SHA384'
uci set openvpn.s2c.cipher='AES-256-CBC'
uci set openvpn.s2c.auth='SHA384'
uci set openvpn.s2c.dev=tun_s2c
uci set openvpn.s2c.server='10.179.43.0 255.255.255.0'
uci set openvpn.s2c.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem'
uci set openvpn.s2c.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
uci set openvpn.s2c.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
uci set openvpn.s2c.dh='/etc/certidude/dh.pem'
uci set openvpn.s2c.enabled=1
uci set openvpn.s2c.comp_lzo=yes
uci add_list openvpn.s2c.push="route-metric 1000"
uci add_list openvpn.s2c.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
uci add_list openvpn.s2c.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
uci add_list openvpn.s2c.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
/etc/init.d/openvpn restart
/etc/init.d/firewall restart</code></pre>
</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>

View File

@ -30,10 +30,11 @@
</button>
<div class="dropdown-menu">
{% for p in session.authority.signature.profiles %}
<a class="dropdown-item" href="/api/request/?organizational_unit={{ p.organizational_unit }}&lifetime={{ p.lifetime }}&flags={{ p.flags }}">
{% if p.organizational_unit %}
{{ p.organizational_unit }} ({{ p.flags }}){% else %}
{{ p.flags | capitalize }}{% endif %}, expires in {{ p.lifetime }} days</a>
<a class="dropdown-item{% if p.server and not request.server %} disabled{% endif %}"
{% if p.server and not request.server %}title="Resubmit with FQDN as common name"{% endif %}
href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.name }}',type:'post'});">
{% if p.title %}{{ p.title }} ({% if p.server %}server{% else %}client{% endif %}){% else %}
{% if p.server %}Server{% else %}Client{% endif %}{% endif %}, expires in {{ p.lifetime }} days</a>
{% endfor %}
</div>
</div>

View File

@ -1,6 +1,10 @@
<p>
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card">
<div class="card-header">
{% if certificate.organizational_unit %}
<i class="fa fa-folder" aria-hidden="true"></i>
{{ certificate.organizational_unit }} /
{% endif %}
{% if certificate.server %}
<i class="fa fa-server"></i>
{% else %}
@ -17,12 +21,9 @@
</span>
Signed
<time class="timeago" datetime="{{ certificate.signed }}">Certificate was signed {{ certificate.signed }}</time>,
<time class="timeago" datetime="{{ certificate.signed }}">Certificate was signed {{ certificate.signed }}</time>{% if certificate.signer %} by {{ certificate.signer }}{% endif %},
expires
<time class="timeago" datetime="{{ certificate.expires }}">Certificate expires {{ certificate.expires }}</time>.
{% if certificate.organizational_unit %}
Part of {{ certificate.organizational_unit }} organizational unit.
{% endif %}
</p>
<p>
{% if session.authority.tagging %}
@ -91,10 +92,10 @@ curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/sign
<div style="overflow: auto; max-width: 100%;">
<table class="table" id="signed_certificates">
<tbody>
<tr><th>Common name</th><td>{{ certificate.common_name }}</td></tr>
<tr><th>Organizational unit</th><td>{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}</td></tr>
<tr><th>Common&nbsp;name</th><td>{{ certificate.common_name }}</td></tr>
<tr><th>Organizational&nbsp;unit</th><td>{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}</td></tr>
<tr><th>Serial number</th><td style="word-wrap:break-word;">{{ certificate.serial | serial }}</td></tr>
<tr><th>Signed</th><td>{{ certificate.signed | datetime }}{% if certificate.signer %}, by {{ certificate.signer }}{% endif %}</td></tr>
<tr><th>Signed</th><td>{{ certificate.signed | datetime }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}</td></tr>
<tr><th>Expires</th><td>{{ certificate.expires | datetime }}</td></tr>
{% if certificate.lease %}
<tr><th>Lease</th><td><a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a> at {{ certificate.lease.last_seen | datetime }}

View File

@ -191,13 +191,13 @@ lifetime = 30
# Secret for generating and validating tokens, regenerate occasionally
secret = {{ token_secret }}
[profile]
# title, flags, lifetime, organizational unit
default = client, 120,
srv = server, 365, Server
gw = server, 3, Gateway
ap = client, 1825, Access Point
# name, flags, lifetime, organizational unit, title
default = client, 120, Roadwarrior, Roadwarrior
gw = server, 30, Gateway, Gateway
srv = server, 365, Server,
ap = client, 1825, Access Point, Access Point
mfp = client, 30, MFP, Printers
[script]
# Path to the folder with scripts that can be served to the clients, set none to disable scripting