diff --git a/certidude/api.py b/certidude/api.py index 4553408..3419f7c 100644 --- a/certidude/api.py +++ b/certidude/api.py @@ -1,4 +1,5 @@ import re +import datetime import falcon import ipaddress import mimetypes @@ -201,9 +202,68 @@ class SignedCertificateDetailResource(CertificateAuthorityBase): def on_delete(self, req, resp, ca, cn, user): ca.revoke(cn) +class LeaseResource(CertificateAuthorityBase): + @serialize + @login_required + @pop_certificate_authority + @authorize_admin + def on_get(self, req, resp, ca, user): + from ipaddress import ip_address + + OIDS = { + (2, 5, 4, 3) : 'CN', # common name + (2, 5, 4, 6) : 'C', # country + (2, 5, 4, 7) : 'L', # locality + (2, 5, 4, 8) : 'ST', # stateOrProvince + (2, 5, 4, 10) : 'O', # organization + (2, 5, 4, 11) : 'OU', # organizationalUnit + } + + def parse_dn(data): + chunks, remainder = decoder.decode(data) + dn = "" + if remainder: + raise ValueError() + # TODO: Check for duplicate entries? + for chunk in chunks: + for chunkette in chunk: + key, value = chunkette + dn += "/" + OIDS[key] + "=" + value + return str(dn) + + # BUGBUG + SQL_LEASES = """ + SELECT + acquired, + released, + address, + identities.data as dn + FROM + addresses + RIGHT JOIN + identities + ON + identities.id = addresses.identity + WHERE + addresses.released <> 1 + """ + cnx = ca.database.get_connection() + cursor = cnx.cursor(dictionary=True) + query = (SQL_LEASES) + cursor.execute(query) + + for row in cursor: + row["acquired"] = datetime.utcfromtimestamp(row["acquired"]) + row["released"] = datetime.utcfromtimestamp(row["released"]) if row["released"] else None + row["address"] = ip_address(bytes(row["address"])) + row["dn"] = parse_dn(bytes(row["dn"])) + yield row + + class SignedCertificateListResource(CertificateAuthorityBase): @serialize @pop_certificate_authority + @authorize_admin @validate_common_name def on_get(self, req, resp, ca): for j in authority.get_signed(): @@ -258,6 +318,7 @@ class RequestDetailResource(CertificateAuthorityBase): class RequestListResource(CertificateAuthorityBase): @serialize @pop_certificate_authority + @authorize_admin def on_get(self, req, resp, ca): for j in ca.get_requests(): yield omit( @@ -458,6 +519,7 @@ def certidude_app(): app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config)) app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config)) app.add_route("/api/ca/{ca}/request/", RequestListResource(config)) + app.add_route("/api/ca/{ca}/lease/", LeaseResource(config)) app.add_route("/api/ca/{ca}/", IndexResource(config)) app.add_route("/api/ca/", AuthorityListResource(config)) return app diff --git a/certidude/auth.py b/certidude/auth.py index e69de29..7956d7e 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -0,0 +1,72 @@ + +import click +import falcon +import kerberos +import os +import re +import socket + +# Vanilla Kerberos provides only username. +# AD also embeds PAC (Privilege Attribute Certificate), which +# is supposed to be sent via HTTP headers and it contains +# the groups user is part of. +# Even then we would have to manually look up the e-mail +# address eg via LDAP, hence to keep things simple +# we simply use Kerberos to authenticate. + +FQDN = socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3] + +if not os.getenv("KRB5_KTNAME"): + click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) + exit(250) + +try: + principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) +except kerberos.KrbError as exc: + click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) + exit(249) +else: + click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) + +def login_required(func): + def wrapped(resource, req, resp, *args, **kwargs): + authorization = req.get_header("Authorization") + + if not authorization: + resp.append_header("WWW-Authenticate", "Negotiate") + raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?") + + token = ''.join(authorization.split()[1:]) + + try: + result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) + except kerberos.GSSError as ex: + raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex[0][0], ex[1][0],)) + + try: + result = kerberos.authGSSServerStep(context, token) + except kerberos.GSSError as ex: + kerberos.authGSSServerClean(context) + raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s(%s)" % (ex[0][0], ex[1][0],)) + except kerberos.KrbError as ex: + kerberos.authGSSServerClean(context) + raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex[0],)) + + kerberos_user = kerberos.authGSSServerUserName(context).split("@") + + try: + # BUGBUG: https://github.com/02strich/pykerberos/issues/6 + #kerberos.authGSSServerClean(context) + pass + except kerberos.GSSError as ex: + raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex[0][0], ex[1][0],)) + + if result == kerberos.AUTH_GSS_COMPLETE: + kwargs["user"] = kerberos_user + return func(resource, req, resp, *args, **kwargs) + elif result == kerberos.AUTH_GSS_CONTINUE: + raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") + else: + raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") + + return wrapped diff --git a/certidude/static/authority.html b/certidude/static/authority.html index 9615696..3c72afd 100644 --- a/certidude/static/authority.html +++ b/certidude/static/authority.html @@ -21,9 +21,9 @@

Signed certificates

-