def _test_public_jwk(key): """ Attempt to read in the key into a key object """ keys = json.loads(key.decode()) public_key_numbers = rsa.RSAPublicNumbers( long_from_bytes(keys['keys'][0]['e']), long_from_bytes(keys['keys'][0]['n'])) return public_key_numbers.public_key(default_backend())
def _test_ec_public_jwk(key): """ Attempt to read in the key into a key object """ keys = json.loads(key.decode('utf-8')) public_key_numbers = ec.EllipticCurvePublicNumbers( long_from_bytes(keys['keys'][0]['x']), long_from_bytes(keys['keys'][0]['y']), ec.SECP256R1()) return public_key_numbers.public_key(default_backend())
def _test_private_jwk(key): """ Attempt to read in the key into a private key object """ keys = json.loads(key.decode()) public_key_numbers = rsa.RSAPublicNumbers( long_from_bytes(keys['keys'][0]['e']), long_from_bytes(keys['keys'][0]['n'])) private_key_numbers = rsa.RSAPrivateNumbers( long_from_bytes(keys['keys'][0]['p']), long_from_bytes(keys['keys'][0]['q']), long_from_bytes(keys['keys'][0]['d']), long_from_bytes(keys['keys'][0]['dp']), long_from_bytes(keys['keys'][0]['dq']), long_from_bytes(keys['keys'][0]['qi']), public_key_numbers) return private_key_numbers.private_key(default_backend())
def deserialize(serialized_token, require_key=False, insecure=False): """ Given a serialized SciToken, load it into a SciTokens object. Verifies the claims pass the current set of validation scripts. :param str serialized_token: The serialized token. :param bool require_key: When True, require the key :param bool insecure: When True, allow insecure methods to verify the issuer, including allowing "localhost" issuer (useful in testing). Default=False """ if require_key is not False: raise NotImplementedError() info = serialized_token.decode('utf8').split(".") if len(info) != 3 and len(info) != 4: # header, format, signature[, key] raise InvalidTokenFormat("Serialized token is not a readable format.") if (len(info) != 4) and require_key: raise MissingKeyException("No key present in serialized token") serialized_jwt = info[0] + "." + info[1] + "." + info[2] unverified_headers = jwt.get_unverified_header(serialized_jwt) unverified_payload = jwt.decode(serialized_jwt, verify=False) # Get the public key from the issuer keycache = scitokens.utils.keycache.KeyCache() issuer_public_key = keycache.getKeyInfo(unverified_payload['iss'], key_id=unverified_headers['kid'], insecure=insecure) claims = jwt.decode(serialized_token, issuer_public_key) # Do we have the private key? if len(info) == 4: to_return = SciToken(key = key) else: to_return = SciToken() to_return._verified_claims = claims to_return._serialized_token = serialized_token return to_return # Clean up all of the below key_decoded = base64.urlsafe_b64decode(key) jwk_dict = json.loads(key_decoded) # TODO: Full range of keytypes and curves from JWK RFC. if (jwk_dict['kty'] != 'EC') or (jwt_dict['crv'] != 'P-256'): raise UnsupportedKeyException("SciToken signed with an unsupported key type") elif 'd' not in jwk_dict: raise UnsupportedKeyException("SciToken key does not contain private number.") if 'pwt' in unverified_headers: pwt = unverified_headers['pwt'] st = SciToken.clone() st.deserialize(pwt, require_key=False) headers = pwt.headers() if 'cwk' not in headers: raise InvalidParentToken("Parent token MUST specify a child JWK.") # Validate the key type / curve matches. TODO: what other headers to check? if (jwk_dict['kty'] != headers['kty']) or (jwk_dict['crv'] != headers['crv']): if 'x' not in jwk_dict: if 'x' in headers: jwk_dict['x'] = headers['x'] else: MissingPublicKeyException("JWK public key is missing 'x'") elif jwk_dict['x'] != headers['x']: raise UnsupportedKeyException("Parent SciToken specifies an incompatible child JWK") if 'y' not in jwk_dict: if 'y' in headers: jwk_dict['y'] = headers['y'] else: MissingPublicKeyException("JWK public key is missing 'y'") elif jwk_dict['y'] != headers['y']: raise UnsupportedKeyException("Parent SciToken specifies an incompatible child JWK") # TODO: Handle non-chained case. elif 'x5u' in unverified_headers: raise NotImplementedError("Non-chained verification is not implemented.") else: raise UnableToValidate("No token validation method available.") public_key_numbers = ec.EllipticCurvePublicNumbers( long_from_bytes(jwk_dict['x']), long_from_bytes(jwk_dict['y']), ec.SECP256R1 ) private_key_numbers = ec.EllipticCurvePrivateNumbers( long_from_bytes(jwk_dict['d']), public_key_numbers ) private_key = private_key_numbers.private_key(backends.default_backend()) public_key = public_key_numbers.public_key(backends.default_backend()) # TODO: check that public and private key match? claims = jwt.decode(serialized_token, public_key, algorithm="EC256")
def _get_issuer_publickey(issuer, key_id=None, insecure=False): """ :return: Tuple containing (public_key, cache_lifetime). Cache_lifetime how the public key is valid """ # Set the user agent so Cloudflare isn't mad at us headers = {'User-Agent': 'SciTokens/{}'.format(PKG_VERSION)} # Go to the issuer's website, and download the OAuth well known bits # https://tools.ietf.org/html/draft-ietf-oauth-discovery-07 well_known_uri = ".well-known/openid-configuration" if not issuer.endswith("/"): issuer = issuer + "/" parsed_url = urlparse.urlparse(issuer) updated_url = urlparse.urljoin(parsed_url.path, well_known_uri) parsed_url_list = list(parsed_url) parsed_url_list[2] = updated_url meta_uri = urlparse.urlunparse(parsed_url_list) # Make sure the protocol is https if not insecure: parsed_url = urlparse.urlparse(meta_uri) if parsed_url.scheme != "https": raise NonHTTPSIssuer( "Issuer is not over HTTPS. RFC requires it to be over HTTPS" ) response = request.urlopen(request.Request(meta_uri, headers=headers)) data = json.loads(response.read().decode('utf-8')) # Get the keys URL from the openid-configuration jwks_uri = data['jwks_uri'] # Now, get the keys if not insecure: parsed_url = urlparse.urlparse(jwks_uri) if parsed_url.scheme != "https": raise NonHTTPSIssuer("jwks_uri is not over HTTPS, insecure!") response = request.urlopen(request.Request(jwks_uri, headers=headers)) # Get the cache data from the headers cache_timer = 0 headers = response.info() if "Cache-Control" in headers: # Parse out the max-age, if it's there. if "max-age" in headers['Cache-Control']: match = re.search(r".*max-age=(\d+)", headers['Cache-Control']) if match: cache_timer = int(match.group(1)) # Minimum cache time of 10 minutes, no matter what the remote says cache_timer = max(cache_timer, config.get_int("cache_lifetime")) keys_data = json.loads(response.read().decode('utf-8')) # Loop through each key, looking for the right key id public_key = "" raw_key = None # If there is no kid in the header, then just take the first key? if key_id == None: if len(keys_data['keys']) != 1: raise NotImplementedError( "No kid in header, but multiple keys in " "response from certs server. Don't know which key to use!" ) else: raw_key = keys_data['keys'][0] else: # Find the right key for key in keys_data['keys']: if key['kid'] == key_id: raw_key = key break if raw_key == None: raise MissingKeyException( "Unable to find key at issuer {}".format(jwks_uri)) if raw_key['kty'] == "RSA": public_key_numbers = rsa.RSAPublicNumbers( long_from_bytes(raw_key['e']), long_from_bytes(raw_key['n'])) public_key = public_key_numbers.public_key( backends.default_backend()) elif raw_key['kty'] == 'EC': public_key_numbers = ec.EllipticCurvePublicNumbers( long_from_bytes(raw_key['x']), long_from_bytes(raw_key['y']), ec.SECP256R1()) public_key = public_key_numbers.public_key( backends.default_backend()) else: raise UnsupportedKeyException( "SciToken signed with an unsupported key type") return public_key, cache_timer