Ejemplo n.º 1
0
    def __call__(self):
        keyjar = self.conv.entity.keyjar
        self.conv.entity.original_keyjar = keyjar.copy()

        # invalidate the old key
        old_kid = self.op_args["old_kid"]
        old_key = keyjar.get_key_by_kid(old_kid)
        old_key.inactive_since = time.time()

        # setup new key
        key_spec = self.op_args["new_key"]
        typ = key_spec["type"].upper()
        if typ == "RSA":
            kb = KeyBundle(keytype=typ, keyusage=key_spec["use"])
            kb.append(RSAKey(use=key_spec["use"]).load_key(
                RSA.generate(key_spec["bits"])))
        elif typ == "EC":
            kb = ec_init(key_spec)
        else:
            raise Exception('Wrong key type')

        # add new key to keyjar with
        list(kb.keys())[0].kid = self.op_args["new_kid"]
        keyjar.add_kb("", kb)

        # make jwks and update file
        keys = []
        for kb in keyjar[""]:
            keys.extend(
                [k.to_dict() for k in list(kb.keys()) if not k.inactive_since])
        jwks = dict(keys=keys)
        with open(self.op_args["jwks_path"], "w") as f:
            f.write(json.dumps(jwks))
Ejemplo n.º 2
0
    def __call__(self):
        keyjar = self.conv.entity.keyjar
        self.conv.entity.original_keyjar = keyjar.copy()

        # invalidate the old key
        old_key_spec = self.op_args["old_key"]
        old_key = keyjar.keys_by_alg_and_usage('', old_key_spec['alg'],
                                               old_key_spec['use'])[0]
        old_key.inactive_since = time.time()

        # setup new key
        key_spec = self.op_args["new_key"]
        typ = key_spec["type"].upper()
        if typ == "RSA":
            kb = KeyBundle(keytype=typ, keyusage=key_spec["use"])
            kb.append(RSAKey(use=key_spec["use"][0]).load_key(
                RSA.generate(key_spec["bits"])))
        elif typ == "EC":
            kb = ec_init(key_spec)
        else:
            raise Unknown('keytype: {}'.format(typ))

        # add new key to keyjar with
        list(kb.keys())[0].kid = self.op_args["new_kid"]
        keyjar.add_kb("", kb)

        # make jwks and update file
        keys = []
        for kb in keyjar[""]:
            keys.extend(
                [k.to_dict() for k in list(kb.keys()) if not k.inactive_since])
        jwks = dict(keys=keys)
        with open(self.op_args["jwks_path"], "w") as f:
            f.write(json.dumps(jwks))
Ejemplo n.º 3
0
    def construct_jwks(_client, key_conf):
        """
        Construct the jwks
        """
        if _client.keyjar is None:
            _client.keyjar = KeyJar()

        kbl = []
        kid_template = "a%d"
        kid = 0
        for typ, info in key_conf.items():
            kb = KeyBundle(source="file://%s" % info["key"], fileformat="der",
                           keytype=typ)

            for k in kb.keys():
                k.serialize()
                k.kid = kid_template % kid
                kid += 1
                _client.kid[k.use][k.kty] = k.kid
            _client.keyjar.add_kb("", kb)

            kbl.append(kb)

        jwks = {"keys": []}
        for kb in kbl:
            # ignore simple keys
            jwks["keys"].extend([k.to_dict()
                                 for k in kb.keys() if k.kty != 'oct'])

        return jwks
Ejemplo n.º 4
0
    def __call__(self, conv, **kwargs):
        # find the name of the file to which the JWKS should be written
        try:
            _uri = conv.client.registration_response["jwks_uri"]
        except KeyError:
            raise RequirementsNotMet("No dynamic key handling")

        r = urlparse(_uri)
        # find the old key for this key usage and mark that as inactive
        for kb in conv.client.keyjar.issuer_keys[""]:
            for key in kb.keys():
                if key.use in self.new_key["use"]:
                    key.inactive = True

        kid = 0
        # only one key
        _nk = self.new_key
        _typ = _nk["type"].upper()

        if _typ == "RSA":
            kb = KeyBundle(source="file://%s" % _nk["key"],
                           fileformat="der",
                           keytype=_typ,
                           keyusage=_nk["use"])
        else:
            kb = {}

        for k in kb.keys():
            k.serialize()
            k.kid = self.kid_template % kid
            kid += 1
            conv.client.kid[k.use][k.kty] = k.kid
        conv.client.keyjar.add_kb("", kb)

        dump_jwks(conv.client.keyjar[""], r.path[1:])
Ejemplo n.º 5
0
def test_dump_private_jwks():
    keys = [
        {
            "type": "RSA",
            "use": ["enc", "sig"]
        },
        {
            "type": "EC",
            "crv": "P-256",
            "use": ["sig"]
        },
    ]

    jwks, keyjar, kidd = build_keyjar(keys)

    kbl = keyjar.issuer_keys['']
    dump_jwks(kbl, 'foo.jwks', private=True)
    kb_public = KeyBundle(source='file://./foo.jwks')
    # All RSA keys
    for k in kb_public.keys():
        if k.kty == 'RSA':
            assert k.d
            assert k.p
            assert k.q
        else:  # MUST be 'EC'
            assert k.d
Ejemplo n.º 6
0
    def __call__(self):
        # find the name of the file to which the JWKS should be written
        try:
            _uri = self.conv.entity.registration_response["jwks_uri"]
        except KeyError:
            raise RequirementsNotMet("No dynamic key handling")

        r = urlparse(_uri)
        # find the old key for this key usage and mark that as inactive
        for kb in self.conv.entity.keyjar.issuer_keys[""]:
            for key in list(kb.keys()):
                if key.use in self.new_key["use"]:
                    key.inactive = True

        kid = 0
        # only one key
        _nk = self.new_key
        _typ = _nk["type"].upper()

        if _typ == "RSA":
            kb = KeyBundle(source="file://%s" % _nk["key"],
                           fileformat="der", keytype=_typ,
                           keyusage=_nk["use"])
        else:
            kb = {}

        for k in list(kb.keys()):
            k.serialize()
            k.kid = self.kid_template % kid
            kid += 1
            self.conv.entity.kid[k.use][k.kty] = k.kid
        self.conv.entity.keyjar.add_kb("", kb)

        dump_jwks(self.conv.entity.keyjar[""], r.path[1:])
Ejemplo n.º 7
0
def test_dump_public_jwks():
    keys = [
        {
            "type": "RSA",
            "use": ["enc", "sig"]
        },
        {
            "type": "EC",
            "crv": "P-256",
            "use": ["sig"]
        },
    ]

    jwks, keyjar, kidd = build_keyjar(keys)

    kbl = keyjar.issuer_keys[""]
    dump_jwks(kbl, "foo.jwks")
    kb_public = KeyBundle(source="file://./foo.jwks")
    # All RSA keys
    for k in kb_public.keys():
        if k.kty == "RSA":
            assert not k.d
            assert not k.p
            assert not k.q
        else:  # MUST be 'EC'
            assert not k.d
Ejemplo n.º 8
0
    def export(self):
        # has to be there
        self.trace.info("EXPORT")

        if self.client.keyjar is None:
            self.client.keyjar = KeyJar()

        kbl = []
        kid_template = "a%d"
        kid = 0
        for typ, info in self.cconf["keys"].items():
            kb = KeyBundle(source="file://%s" % info["key"], fileformat="der", keytype=typ)

            for k in kb.keys():
                k.serialize()
                k.kid = kid_template % kid
                kid += 1
                self.client.kid[k.use][k.kty] = k.kid
            self.client.keyjar.add_kb("", kb)

            kbl.append(kb)

        try:
            new_name = "static/jwks.json"
            dump_jwks(kbl, new_name)
            self.client.jwks_uri = "%s%s" % (self.cconf["_base_url"], new_name)
        except KeyError:
            pass

        if self.args.internal_server:
            self._pop = start_key_server(self.cconf["_base_url"], self.args.script_path or None)
            self.environ["keyprovider"] = self._pop
            self.trace.info("Started key provider")
            time.sleep(1)
Ejemplo n.º 9
0
    def export(self, client, cconf, role):
        # has to be there
        self.trace.info("EXPORT")

        if client.keyjar is None:
            client.keyjar = KeyJar()

        kbl = []
        for typ, info in cconf["keys"].items():
            kb = KeyBundle(source="file://%s" % info["key"],
                           fileformat="der", keytype=typ)
            for k in kb.keys():
                k.serialize()
            client.keyjar.add_kb("", kb)
            kbl.append(kb)

        try:
            new_name = "static/%s_jwks.json" % role
            dump_jwks(kbl, new_name)
            client.jwks_uri = "%s%s" % (cconf["_base_url"], new_name)
        except KeyError:
            pass

        if not self.args.external_server and not self.keysrv_running:
            self._pop = start_key_server(cconf["_base_url"])

            self.environ["keyprovider"] = self._pop
            self.trace.info("Started key provider")
            time.sleep(1)
            self.keysrv_running = True
Ejemplo n.º 10
0
    def export(self, client, cconf, role):
        # has to be there
        self.trace.info("EXPORT")

        if client.keyjar is None:
            client.keyjar = KeyJar()

        kbl = []
        for typ, info in cconf["keys"].items():
            kb = KeyBundle(source="file://%s" % info["key"],
                           fileformat="der",
                           keytype=typ)
            for k in kb.keys():
                k.serialize()
            client.keyjar.add_kb("", kb)
            kbl.append(kb)

        try:
            new_name = "static/%s_jwks.json" % role
            dump_jwks(kbl, new_name)
            client.jwks_uri = "%s%s" % (cconf["_base_url"], new_name)
        except KeyError:
            pass

        if not self.args.external_server and not self.keysrv_running:
            self._pop = start_key_server(cconf["_base_url"])

            self.environ["keyprovider"] = self._pop
            self.trace.info("Started key provider")
            time.sleep(1)
            self.keysrv_running = True
Ejemplo n.º 11
0
    def store_key(self, key):
        kb = KeyBundle()
        kb.do_keys([key])

        # Store key with thumbprint as key
        key_thumbprint = b64e(kb.keys()[0].thumbprint("SHA-256")).decode("utf8")
        self.thumbprint2key[key_thumbprint] = key
        return key_thumbprint
Ejemplo n.º 12
0
    def store_key(self, key):
        kb = KeyBundle()
        kb.do_keys([key])

        # Store key with thumbprint as key
        key_thumbprint = b64e(kb.keys()[0].thumbprint('SHA-256')).decode(
            'utf8')
        self.thumbprint2key[key_thumbprint] = key
        return key_thumbprint
Ejemplo n.º 13
0
def test_dump_private_jwks():
    keys = [
        {"type": "RSA", "use": ["enc", "sig"]},
        {"type": "EC", "crv": "P-256", "use": ["sig"]},
    ]

    jwks, keyjar, kidd = build_keyjar(keys)

    kbl = keyjar.issuer_keys['']
    dump_jwks(kbl, 'foo.jwks', private=True)
    kb_public = KeyBundle(source='file://./foo.jwks')
    # All RSA keys
    for k in kb_public.keys():
        if k.kty == 'RSA':
            assert k.d
            assert k.p
            assert k.q
        else:  # MUST be 'EC'
            assert k.d
Ejemplo n.º 14
0
    def __call__(self):
        # find the name of the file to which the JWKS should be written
        try:
            _uri = self.conv.entity.registration_response["jwks_uri"]
        except KeyError:
            raise RequirementsNotMet("No dynamic key handling")

        r = urlparse(_uri)
        # find the old key for this key usage and mark that as inactive
        for kb in self.conv.entity.keyjar.issuer_keys[""]:
            for key in list(kb.keys()):
                if key.use in self.new_key["use"]:
                    key.inactive = True

        kid = 0
        # only one key
        _nk = self.new_key
        _typ = _nk["type"].upper()

        if _typ == "RSA":
            error_to_catch = getattr(builtins, 'FileNotFoundError',
                                     getattr(builtins, 'IOError'))
            try:
                kb = KeyBundle(source="file://%s" % _nk["key"],
                               fileformat="der",
                               keytype=_typ,
                               keyusage=_nk["use"])
            except error_to_catch:
                kb = _new_rsa_key(_nk)
        else:
            kb = {}

        for k in list(kb.keys()):
            k.serialize()
            k.add_kid()
            self.conv.entity.kid[k.use][k.kty] = k.kid
        self.conv.entity.keyjar.add_kb("", kb)

        dump_jwks(self.conv.entity.keyjar[""], r.path[1:])
Ejemplo n.º 15
0
    def export(self):
        # has to be there
        self.trace.info("EXPORT")

        if self.client.keyjar is None:
            self.client.keyjar = KeyJar()

        kbl = []
        kid_template = "a%d"
        kid = 0
        for typ, info in self.cconf["keys"].items():
            kb = KeyBundle(source="file://%s" % info["key"],
                           fileformat="der",
                           keytype=typ)

            for k in kb.keys():
                k.serialize()
                k.kid = kid_template % kid
                kid += 1
                self.client.kid[k.use][k.kty] = k.kid
            self.client.keyjar.add_kb("", kb)

            kbl.append(kb)

        try:
            new_name = "static/jwks.json"
            dump_jwks(kbl, new_name)
            self.client.jwks_uri = "%s%s" % (self.cconf["_base_url"], new_name)
        except KeyError:
            pass

        if self.args.internal_server:
            self._pop = start_key_server(self.cconf["_base_url"],
                                         self.args.script_path or None)
            self.environ["keyprovider"] = self._pop
            self.trace.info("Started key provider")
            time.sleep(1)
Ejemplo n.º 16
0
class InAcademiaMediator(object):
    """The main CherryPy application, with all exposed endpoints.

    This app mediates between a OpenIDConnect provider front-end, which uses SAML as the back-end for authenticating
    users.
    """

    def __init__(self, base_url, op, sp):
        self.base_url = base_url
        self.op = op
        self.sp = sp

        # Setup key for encrypting/decrypting the state (passed in the SAML RelayState).
        source = "file://symkey.json"
        self.key_bundle = KeyBundle(source=source, fileformat="jwk")

        for key in self.key_bundle.keys():
            key.deserialize()

    @cherrypy.expose
    def index(self):
        raise cherrypy.HTTPRedirect("http://www.inacademia.org")

    @cherrypy.expose
    def status(self):
        return

    @cherrypy.expose
    def authorization(self, *args, **kwargs):
        """Where the OP Authentication Request arrives.
        """
        transaction_session = self.op.verify_authn_request(cherrypy.request.query_string)
        state = self._encode_state(transaction_session)

        log_transaction_start(logger, cherrypy.request, state, transaction_session["client_id"],
                              transaction_session["scope"],
                              transaction_session["redirect_uri"])
        return self.sp.redirect_to_auth(state, transaction_session["scope"])

    @cherrypy.expose
    def disco(self, state=None, entityID=None, **kwargs):
        """Where the SAML Discovery Service response arrives.
        """
        if state is None:
            raise cherrypy.HTTPError(404, _('Page not found.'))

        transaction_session = self._decode_state(state)
        if "error" in kwargs:
            abort_with_client_error(state, transaction_session, cherrypy.request, logger,
                                    "Discovery service error: '{}'.".format(kwargs["error"]))
        elif entityID is None or entityID == "":
            abort_with_client_error(state, transaction_session, cherrypy.request, logger,
                                    "No entity id returned from discovery server.")

        return self.sp.disco(entityID, state, transaction_session)

    @cherrypy.expose
    def error(self, lang=None, error=None):
        """Where the i18n of the error page is handled.
        """
        if error is None:
            raise cherrypy.HTTPError(404, _("Page not found."))

        self._set_language(lang)

        error = json.loads(urllib.unquote_plus(error))
        raise EndUserErrorResponse(**error)

    def webfinger(self, rel=None, resource=None):
        """Where the WebFinger request arrives.

        This function is mapped explicitly using PathDiscpatcher.
        """

        try:
            assert rel == OIC_ISSUER
            assert resource is not None
        except AssertionError as e:
            raise cherrypy.HTTPError(400, "Missing or incorrect parameter in webfinger request.")

        cherrypy.response.headers["Content-Type"] = "application/jrd+json"
        return WebFinger().response(resource, self.op.OP.baseurl)

    def openid_configuration(self):
        """Where the OP configuration request arrives.

        This function is mapped explicitly using PathDispatcher.
        """

        return response_to_cherrypy(self.op.OP.providerinfo_endpoint())

    def consent_allow(self, state=None, released_claims=None):
        """Where the approved consent arrives.

        This function is mapped explicitly using PathDispatcher.
        """
        if state is None or released_claims is None:
            raise cherrypy.HTTPError(404, _("Page not found."))

        state = json.loads(urllib.unquote_plus(state))
        released_claims = json.loads(urllib.unquote_plus(released_claims))
        transaction_session = self._decode_state(state["state"])
        log_internal(logger, "consented claims: {}".format(json.dumps(released_claims)),
                     cherrypy.request, state["state"], transaction_session["client_id"])
        return self.op.id_token(released_claims, state["idp_entity_id"], state["state"],
                                transaction_session)

    def consent_deny(self, state=None, released_claims=None):
        """Where the denied consent arrives.

        This function is mapped explicitly using PathDispatcher.
        """
        if state is None:
            raise cherrypy.HTTPError(404, _("Page not found."))

        state = json.loads(urllib.unquote_plus(state))
        transaction_session = self._decode_state(state["state"])
        negative_transaction_response(state["state"], transaction_session, cherrypy.request, logger,
                                      "User did not give consent.", state["idp_entity_id"])

    def consent_index(self, lang=None, state=None, released_claims=None):
        """Where the i18n of the consent page arrives.

        This function is mapped explicitly using PathDispatcher.
        """
        if state is None or released_claims is None:
            raise cherrypy.HTTPError(404, _("Page not found."))

        self._set_language(lang)

        state = json.loads(urllib.unquote_plus(state))
        rp_client_id = self._decode_state(state["state"])["client_id"]
        released_claims = json.loads(urllib.unquote_plus(released_claims))

        client_name = self._get_client_name(rp_client_id)
        return ConsentPage.render(client_name, state["idp_entity_id"], released_claims,
                                  state["state"])

    def acs_post(self, SAMLResponse=None, RelayState=None, **kwargs):
        """Where the SAML Authentication Response arrives.

        This function is mapped explicitly using PathDiscpatcher.
        """
        return self._acs(SAMLResponse, RelayState, BINDING_HTTP_POST)

    def acs_redirect(self, SAMLResponse=None, RelayState=None):
        """Where the SAML Authentication Response arrives.
        """

        return self._acs(SAMLResponse, RelayState, BINDING_HTTP_REDIRECT)

    def _acs(self, SAMLResponse, RelayState, binding):
        """Handle the SAMLResponse from the IdP and produce the consent page.

        :return: HTML of the OP consent page.
        """
        transaction_session = self._decode_state(RelayState)
        user_id, affiliation, identity, auth_time, idp_entity_id = self.sp.acs(SAMLResponse,
                                                                               binding, RelayState,
                                                                               transaction_session)

        # if we have passed all checks, ask the user for consent before finalizing
        released_claims = self.op.get_claims_to_release(user_id, affiliation, identity, auth_time,
                                                        idp_entity_id,
                                                        self.sp.metadata, transaction_session)
        log_internal(logger, "claims to consent: {}".format(json.dumps(released_claims)),
                     cherrypy.request, RelayState, transaction_session["client_id"])

        client_name = self._get_client_name(transaction_session["client_id"])
        return ConsentPage.render(client_name, idp_entity_id, released_claims, RelayState)

    def _set_language(self, lang):
        """Set the language.
        """
        if lang is None:
            lang = "en"

        # Modify the Accept-Language header and use the CherryPy i18n tool for translation
        cherrypy.request.headers["Accept-Language"] = lang
        i18n_args = {
            "default": cherrypy.config["tools.I18nTool.default"],
            "mo_dir": cherrypy.config["tools.I18nTool.mo_dir"],
            "domain": cherrypy.config["tools.I18nTool.domain"]
        }
        cherrypy.tools.I18nTool.callable(**i18n_args)

    def _decode_state(self, state):
        """Decode the transaction data.

        If the state can not be decoded, the transaction will fail with error page for the user. We can't
        notify the client since the transaction state now is unknown.
        """
        try:
            return deconstruct_state(state, self.key_bundle.keys())
        except DecryptionFailed as e:
            abort_with_enduser_error(state, "-", cherrypy.request, logger,
                                     _(
                                             "We could not complete your validation because an error occurred while handling "
                                             "your request. Please return to the service which initiated the validation "
                                             "request and try again."),
                                     "Transaction state missing or broken in incoming response.")

    def _encode_state(self, payload):
        """Encode the transaction data.
        """
        _kids = self.key_bundle.kids()
        _kids.sort()

        return construct_state(payload, self.key_bundle.get_key_with_kid(_kids[-1]))

    def _get_client_name(self, client_id):
        """Get the display name for the client.

        :return: the clients display name, or client_id if no display name is known.
        """
        try:
            client_info = self.op.OP.cdb[client_id]
            return client_info.get("display_name", client_id)
        except KeyError as e:
            return client_id