# HG changeset patch # User Tero Marttila # Date 1389726210 -7200 # Node ID 6beb06b59ee67b24f67de493a3581f04d4500133 # Parent 24e6c32c488f6c1727a62aabe2203eb6e2525bdb pvl.login: do not store invalid pubtkt's in self.pubtkt; implement a ssl client cert ca diff -r 24e6c32c488f -r 6beb06b59ee6 .hgignore --- a/.hgignore Tue Jan 14 21:02:54 2014 +0200 +++ b/.hgignore Tue Jan 14 21:03:30 2014 +0200 @@ -8,6 +8,7 @@ ^dist/ ^MANIFEST$ ^log +^ssl/ # testing ^(tmp|test|var|opt)/ diff -r 24e6c32c488f -r 6beb06b59ee6 bin/pvl.login-server --- a/bin/pvl.login-server Tue Jan 14 21:02:54 2014 +0200 +++ b/bin/pvl.login-server Tue Jan 14 21:03:30 2014 +0200 @@ -9,6 +9,7 @@ import pvl.ldap.args import pvl.login.auth import pvl.login.server +import pvl.login.ssl import pvl.web.args @@ -35,7 +36,8 @@ # app application = pvl.web.args.apply(options, pvl.login.server.LoginApplication, - auth=pvl.login.auth.LDAPAuth(ldap), + auth = pvl.login.auth.LDAPAuth(ldap), + ssl = pvl.login.ssl.UsersCA('ssl/userca', 'ssl/users'), ) # behind a reverse-proxy diff -r 24e6c32c488f -r 6beb06b59ee6 pvl/login/server.py --- a/pvl/login/server.py Tue Jan 14 21:02:54 2014 +0200 +++ b/pvl/login/server.py Tue Jan 14 21:03:30 2014 +0200 @@ -44,6 +44,8 @@ return pvl.web.response.redirect(self.url(*url, **params)) pubtkt = None + invalid_pubtkt = None + valid_pubtkt = None def init (self) : self.alerts = [] @@ -80,13 +82,22 @@ except pubtkt.ParseError as ex : self.alert('danger', ex, icon='compare') + except pubtkt.VerifyError as ex : + self.alert('danger', ex, icon='warning-sign') + + self.invalid_pubtkt = ex.pubtkt + except pubtkt.ExpiredError as ex : - self.pubtkt = ex.pubtkt self.alert('warning', ex, icon='clock') + + # store it anyways, but not as valid + self.pubtkt = ex.pubtkt - except pubtkt.VerifyError as ex : - self.pubtkt = ex.pubtkt - self.alert('danger', ex, icon='warning-sign') + else : + # it's a parsed, verified and valid pubtkt + self.valid_pubtkt = self.pubtkt + + return self.pubtkt def process_back (self) : self.server = None @@ -106,8 +117,12 @@ else : self.alert('info', "Using SSL for application URL", icon='lock') scheme = self.app.login_scheme - - self.server = self.app.check_server(url.hostname) + + if url.hostname : + self.server = self.app.check_server(url.hostname) + else : + self.server = self.app.login_server + self.back = urlparse.urlunparse((scheme, self.server, url.path, url.params, url.query, url.fragment)) def render_alerts (self) : @@ -454,6 +469,8 @@ def render (self) : return html.div(class_='container')( html.form(action=self.url(), method='post')( + self.render_alerts(), + html.fieldset( html.legend("Logout {pubtkt.uid}".format(pubtkt=self.pubtkt)), @@ -464,11 +481,61 @@ ) ) +class SSL (Handler) : + TITLE = "SSL" + + OUT = 'tmp/spkac' + + def process_spkac (self, spkac) : + log.info("SPKAC: %s", spkac) + + cert = self.app.sign_ssl(self.pubtkt, spkac) + + file = self.response_file(open(cert)) + + log.info("Returning client cert: %s", cert) + + return pvl.web.Response(file, mimetype='application/x-x509-user-cert') + + def process (self) : + if not self.process_cookie() : + return self.redirect(Login, back=self.url()) + + self.sslcert_dn = self.request.headers.get('X-Forwarded-SSL-DN') + + if self.request.method == 'POST' : + spkac = self.request.form.get('spkac') + + if spkac: + return self.process_spkac(spkac) + + def render (self) : + if self.sslcert_dn : + self.alert('info', "You are currently using a client SSL cert: {self.sslcert_dn}".format(self=self)) + + return html.div(class_='container')( + html.form(action=self.url(), method='post')( + self.render_alerts(), + html.fieldset( + html.legend("SSL Login"), + + html.keygen(name='spkac', challenge='foo', keytype='RSA'), + + html.button(type='submit', class_='btn')( + "Generate Certificate" + ), + ) + ) + ) + class LoginApplication (pvl.web.Application) : URLS = urls.Map(( urls.rule('/', Index), urls.rule('/login', Login), urls.rule('/logout', Logout), + + # proto + urls.rule('/ssl', SSL), )) PUBLIC_KEY = 'etc/login/public.pem' @@ -485,10 +552,11 @@ cookie_secure = True cookie_httponly = True - def __init__ (self, auth, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) : + def __init__ (self, auth, ssl=None, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) : super(LoginApplication, self).__init__(**opts) self._auth = auth + self._ssl = ssl self.server_keys = pubtkt.ServerKeys.config( public_key = public_key, private_key = private_key, @@ -555,3 +623,15 @@ tokens = list(self._auth.access(auth)), udata = self._auth.userdata(auth), ) + + def sign_ssl (self, pubtkt, spkac) : + """ + Generate a SSL client cert for the given user. + """ + + if not self._ssl : + raise pvl.login.ssl.Error("No ssl CA available for signing") + + return self._ssl.sign_user(pubtkt.uid, spkac, + userinfo = pubtkt.udata, + ) diff -r 24e6c32c488f -r 6beb06b59ee6 pvl/login/ssl.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/login/ssl.py Tue Jan 14 21:03:30 2014 +0200 @@ -0,0 +1,134 @@ +# encoding: utf-8 + +import base64 +import datetime +import hashlib +import os +import os.path +import string + +import pvl.invoke + +import logging; log = logging.getLogger('pvl.login.ssl') + +class Error (Exception) : + pass + +class UsersCA (object) : + OPENSSL = '/usr/bin/openssl' + + SIGN_DAYS = 1 + + VALID_USER = set(string.letters + string.digits + '-.') + + O = u"Päivölän Kansanopisto" + OU = u"People" + DC = ('paivola', 'fi') + + def __init__ (self, ca, users) : + self.ca = ca + self.users = users + + self.ca_config = os.path.join(ca, 'openssl.cnf') + + def sign_spkac (self, out, spkac, days=SIGN_DAYS) : + """ + Sign given request file (path). + + Creates the given output file (path). Empty file on errors.. + """ + + pvl.invoke.invoke(self.OPENSSL, ('ca', + '-config', self.ca_config, + '-spkac', spkac, + '-out', out, + '-policy', 'policy_user', + '-days', str(days), + '-utf8', + ), + setenv={ + 'CA': self.ca, + }, + ) + + def generate_dn (self, uid, cn=None) : + """ + Generate OpenSSL (rdn, value) pairs for given user. + """ + + if self.O : + yield 'O', self.O + + elif self.DC : + for index, dc in enumerate(self.DC, 1) : + yield '{index}.DC'.format(index=index), dc + + yield 'OU', self.OU + + yield 'UID', uid + + if cn : + yield 'CN', cn + + def write_spkac (self, path, spkac, dn) : + """ + Write out a spkac file to the given path, containing the given base64-encoded spkac and DN. + """ + + # roundtrip the spkac for consistent formatting + spkac = base64.b64encode(base64.b64decode(spkac)) + + file = open(path, 'w') + + file.write('SPKAC=') + file.write(spkac) + file.write('\n') + + for rdn, value in dn : + file.write(u'{rdn}={value}\n'.format(rdn=rdn, value=value).encode('utf-8')) + + file.close() + + def sign_user (self, user, spkac, userinfo=None) : + """ + Sign given spkac string (base64-encoded) for given user. + + Returns path to the signed cert. + """ + + if not set(user).issubset(self.VALID_USER) : + raise Error("Invalid username: {user}".format(user=user)) + + dir = os.path.join(self.users, user) + + if not os.path.exists(dir) : + os.mkdir(dir) + + name = hashlib.sha1(user + spkac).hexdigest() + spkac_file = os.path.join(dir, name) + '.spkac' + cert_file = os.path.join(dir, name) + tmp_file = os.path.join(dir, name) + '.tmp' + + # the req to sign + if os.path.exists(spkac_file) : + log.warning("spkac already exists: %s", spkac_file) + else : + log.info("%s: write spkac: %s", user, spkac_file) + self.write_spkac(os.path.join(dir, name) + '.spkac', spkac, self.generate_dn(user, userinfo)) + + # sign it + if os.path.exists(cert_file) : + log.warning("cert already exists: %s", cert_file) + return cert_file + + if os.path.exists(tmp_file) : + log.warning("cleaning out previous tmp file: %s", tmp_file) + os.unlink(tmp_file) + + log.info("%s: sign cert: %s", user, cert_file) + self.sign_spkac(tmp_file, spkac_file) + + log.debug("%s: rename %s -> %s", user, tmp_file, cert_file) + os.rename(tmp_file, cert_file) + + return cert_file