certidude/certidude/api/token.py

96 lines
4.0 KiB
Python
Raw Normal View History

import falcon
2017-04-21 21:22:08 +00:00
import logging
import hashlib
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
2017-04-21 21:22:08 +00:00
from datetime import datetime
from time import time
from certidude import mailer
from certidude.decorators import serialize
2017-04-21 21:22:08 +00:00
from certidude.user import User
from certidude import config
from .utils import AuthorityHandler
from .utils.firewall import login_required, authorize_admin
2017-04-21 21:22:08 +00:00
logger = logging.getLogger(__name__)
class TokenResource(AuthorityHandler):
def on_put(self, req, resp):
2017-04-21 21:22:08 +00:00
# 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"))
2017-04-21 21:22:08 +00:00
2017-04-24 17:33:55 +00:00
margin = 300 # Tolerate 5 minute clock skew as Kerberos does
2017-04-21 21:22:08 +00:00
if csum.hexdigest() != req.get_param("c", required=True):
2017-04-26 06:13:41 +00:00
raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?")
2017-04-24 17:33:55 +00:00
if now < timestamp - margin:
2017-04-26 06:13:41 +00:00
raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?")
2017-04-24 17:33:55 +00:00
if now > timestamp + margin + config.TOKEN_LIFETIME:
2017-04-26 06:13:41 +00:00
raise falcon.HTTPForbidden("Forbidden", "Token expired")
2017-04-21 21:22:08 +00:00
# At this point consider token to be legitimate
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",
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)
except FileExistsError:
logger.info("Won't autosign duplicate %s", common_name)
raise falcon.HTTPConflict(
"Certificate with such common name (CN) already exists",
"Will not overwrite existing certificate signing request, explicitly delete existing one and try again")
2017-04-21 21:22:08 +00:00
@serialize
2017-04-21 21:22:08 +00:00
@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
2017-04-21 21:22:08 +00:00
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)
2017-04-26 06:13:41 +00:00
try:
with open("/etc/timezone") as fh:
token_timezone = fh.read().strip()
except EnvironmentError:
token_timezone = None
url = "%s#%s" % (config.TOKEN_URL, args)
2017-04-21 21:22:08 +00:00
context = globals()
context.update(locals())
mailer.send("token.md", to=user, **context)
return {
"token": args,
"url": url,
}