Add token based auth for profiles

This commit is contained in:
Lauri Võsandi 2017-04-21 21:22:08 +00:00
parent 9a793088c6
commit 0344141faf
7 changed files with 148 additions and 70 deletions

View File

@ -190,6 +190,7 @@ def certidude_app():
from .tag import TagResource, TagDetailResource
from .attrib import AttributeResource
from .bootstrap import BootstrapResource
from .token import TokenResource
app = falcon.API(middleware=NormalizeMiddleware())
app.req_options.auto_parse_form_urlencoded = True
@ -202,7 +203,9 @@ def certidude_app():
app.add_route("/api/request/{cn}/", RequestDetailResource())
app.add_route("/api/request/", RequestListResource())
app.add_route("/api/", SessionResource())
app.add_route("/api/bootstrap/", BootstrapResource())
if config.BUNDLE_FORMAT and config.USER_ENROLLMENT_ALLOWED:
app.add_route("/api/token/", TokenResource())
# Extended attributes for scripting etc.
app.add_route("/api/signed/{cn}/attr/", AttributeResource())
@ -217,8 +220,7 @@ def certidude_app():
# Gateways can submit leases via this API call
app.add_route("/api/lease/", LeaseResource())
# Optional user enrollment API call
if config.USER_ENROLLMENT_ALLOWED:
app.add_route("/api/bundle/", BundleResource())
# Bootstrap resource
app.add_route("/api/bootstrap/", BootstrapResource())
return app

View File

@ -1,44 +0,0 @@
import logging
import hashlib
from certidude import config, authority
from certidude.auth import login_required
logger = logging.getLogger(__name__)
KEYWORDS = (
(u"Android", u"android"),
(u"iPhone", u"iphone"),
(u"iPad", u"ipad"),
(u"Ubuntu", u"ubuntu"),
(u"Fedora", u"fedora"),
(u"Linux", u"linux"),
(u"Macintosh", u"mac"),
)
class BundleResource(object):
@login_required
def on_get(self, req, resp):
common_name = req.context["user"].name
if config.USER_MULTIPLE_CERTIFICATES:
for key, value in KEYWORDS:
if key in req.user_agent:
device_identifier = value
break
else:
device_identifier = u"unknown-device"
common_name = u"%s@%s-%s" % (common_name, device_identifier, \
hashlib.sha256(req.user_agent).hexdigest()[:8])
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
if config.BUNDLE_FORMAT == "p12":
resp.set_header("Content-Type", "application/x-pkcs12")
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user"))
elif config.BUNDLE_FORMAT == "ovpn":
resp.set_header("Content-Type", "application/x-openvpn")
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
resp.body, cert = authority.generate_ovpn_bundle(common_name,
owner=req.context.get("user"))
else:
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)

94
certidude/api/token.py Normal file
View File

@ -0,0 +1,94 @@
import click
import logging
import hashlib
import random
import string
from datetime import datetime
from time import time
from certidude import mailer
from certidude.user import User
from certidude import config, authority
from certidude.auth import login_required, authorize_admin
logger = logging.getLogger(__name__)
chars = string.ascii_letters + string.digits + '!@#$%^&*()'
SECRET = ''.join(random.choice(chars) for i in range(32))
click.echo("Token secret: %s" % SECRET)
KEYWORDS = (
(u"Android", u"android"),
(u"iPhone", u"iphone"),
(u"iPad", u"ipad"),
(u"Ubuntu", u"ubuntu"),
(u"Fedora", u"fedora"),
(u"Linux", u"linux"),
(u"Macintosh", u"mac"),
)
class TokenResource(object):
def on_get(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(SECRET)
csum.update(username)
csum.update(str(timestamp))
if csum.hexdigest() != req.get_param("c", required=True):
raise # TODO
if now < timestamp:
raise # Token not valid yet
if now > timestamp + config.TOKEN_LIFETIME:
raise # token expired
# At this point consider token to be legitimate
common_name = username
if config.USER_MULTIPLE_CERTIFICATES:
for key, value in KEYWORDS:
if key in req.user_agent:
device_identifier = value
break
else:
device_identifier = u"unknown-device"
common_name = u"%s@%s-%s" % (common_name, device_identifier, \
hashlib.sha256(req.user_agent).hexdigest()[:8])
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
if config.BUNDLE_FORMAT == "p12":
resp.set_header("Content-Type", "application/x-pkcs12")
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user"))
elif config.BUNDLE_FORMAT == "ovpn":
resp.set_header("Content-Type", "application/x-openvpn")
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
resp.body, cert = authority.generate_ovpn_bundle(common_name,
owner=req.context.get("user"))
else:
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)
@login_required
@authorize_admin
def on_post(self, req, resp):
# Generate token
issuer = req.context.get("user")
username = req.get_param("user", required=True)
user = User.objects.get(username)
timestamp = int(time())
csum = hashlib.sha256()
csum.update(SECRET)
csum.update(username)
csum.update(str(timestamp))
args = "u=%s&t=%d&c=%s" % (username, timestamp, csum.hexdigest())
token_created = datetime.utcfromtimestamp(timestamp)
token_expires = datetime.utcfromtimestamp(timestamp + config.TOKEN_LIFETIME)
context = globals()
context.update(locals())
mailer.send("token.md", to=user, **context)

View File

@ -44,8 +44,6 @@ EXPIRED_DIR = cp.get("authority", "expired dir")
MAILER_NAME = cp.get("mailer", "name")
MAILER_ADDRESS = cp.get("mailer", "address")
BUNDLE_FORMAT = cp.get("bundle", "format")
OPENVPN_PROFILE_TEMPLATE = cp.get("bundle", "openvpn profile template")
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
MACHINE_ENROLLMENT_ALLOWED = {
@ -96,4 +94,10 @@ else:
TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")]
# Tokens
BUNDLE_FORMAT = cp.get("token", "format")
OPENVPN_PROFILE_TEMPLATE = cp.get("token", "openvpn profile template")
TOKEN_URL = cp.get("token", "url")
TOKEN_LIFETIME = cp.getint("token", "lifetime")
# TODO: Check if we don't have base or servers

View File

@ -12,16 +12,17 @@ from urlparse import urlparse
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
def send(template, to=None, attachments=(), **context):
def send(template, to=None, include_admins=True, attachments=(), **context):
from certidude import authority, config
if not config.MAILER_ADDRESS:
# Mailbox disabled, don't send e-mail
return
recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()])
recipients = ()
if include_admins:
recipients = tuple(User.objects.filter_admins())
if to:
recipients = to + u", " + recipients
recipients = (to,) + recipients
click.echo("Sending e-mail %s to %s" % (template, recipients))
@ -29,12 +30,12 @@ def send(template, to=None, attachments=(), **context):
html = markdown(text)
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["Subject"] = subject.encode("utf-8")
msg["From"] = "%s <%s>" % (config.MAILER_NAME, config.MAILER_ADDRESS)
msg["To"] = recipients
msg["To"] = ", ".join([unicode(j) for j in recipients]).encode("utf-8")
part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
part1 = MIMEText(text.encode("utf-8"), "plain", "utf-8")
part2 = MIMEText(html.encode("utf-8"), "html", "utf-8")
msg.attach(part1)
msg.attach(part2)
@ -44,6 +45,7 @@ def send(template, to=None, attachments=(), **context):
part.add_header('Content-Disposition', 'attachment', filename=suggested_filename)
part.set_payload(attachment)
msg.attach(part)
click.echo("Sending to: %s" % msg["to"])
conn = smtplib.SMTP("localhost")
conn.sendmail(config.MAILER_ADDRESS, recipients, msg.as_string())
conn.sendmail(config.MAILER_ADDRESS, [u.mail for u in recipients], msg.as_string())

View File

@ -1,5 +1,18 @@
Stored request {{ common_name }}
Token for setting up VPN
This is simply to notify that certificate signing request for {{ common_name }}
was stored. You may log in with a certificate authority administration account to sign it.
{{ issuer }} has provided {{ user }} a token for retrieving
profile from the link below.
{% if config.BUNDLE_FORMAT == "ovpn" %}
To set up OpenVPN for your device:
* for Android install [OpenVPN Connect](https://play.google.com/store/apps/details?id=de.blinkt.openvpn) app. After importing the OpenVPN profile in OpenVPN application and delete the downloaded .ovpn file.
* for iOS device install [OpenVPN Connect](https://itunes.apple.com/us/app/openvpn-connect/id590379981) app. Tap on the token URL below, it should be automatically opened with OpenVPN Connect app. Tap connect to establish connection.
* for Mac OS X download [Tunnelblick](https://tunnelblick.net/downloads.html)
* for Ubuntu and Fedora install OpenVPN plugin for NetworkManager. Open network settings, add connection and select "Import from file ...". Supply the file retrieved via the token URL below.
* for Windows you need to install OpenVPN community edition from [here](https://swupdate.openvpn.org/community/releases/openvpn-install-2.3.14-I601-x86_64.exe) and TAP driver from [here](https://swupdate.openvpn.org/community/releases/tap-windows-9.21.2.exe)
{% endif %}
Click [here]({{ config.TOKEN_URL }}?{{ args }}) to claim the token.
Token is usable until {{ token_expires }} (UTC).

View File

@ -141,14 +141,6 @@ name = Certificate management
address =
;address = certificates@example.com
[bundle]
format = p12
;format = ovpn
# Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf
# to /etc/certidude/ and make modifications as necessary
openvpn profile template = {{ template_path }}/openvpn-client.conf
[tagging]
owner/string = Owner
location/string = Location
@ -160,3 +152,18 @@ other/ = Other
# Services template is rendered on certidude server with relevant variables and
# placed to /etc/certidude/services.conf on the client
services template = {{ template_path }}/bootstrap.conf
[token]
# Token mechanism allows authority administrator to send invites for users.
# Token URL could be for example exposed on the internet via proxypass.
url = http://{{ common_name }}/api/token
lifetime = 3600
# Profile format, uncomment specific one to enable token mechanism
format =
;format = p12
;format = ovpn
# Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf
# to /etc/certidude/ and make modifications as necessary
openvpn profile template = {{ template_path }}/openvpn-client.conf