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))
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))
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
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:])
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
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:])
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
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)
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
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
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
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
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:])
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