def test_RSAKey_errors(self): rsa_key = { "kty": "RSA", "kid": "*****@*****.**", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB" } with pytest.raises(JWKError): key = jwk.RSAKey(rsa_key, 'HS256') rsa_key = { "kty": "oct", "kid": "*****@*****.**", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB" } with pytest.raises(JWKError): key = jwk.RSAKey(rsa_key, 'RS256')
def test_invalid_hash_alg(self): with pytest.raises(JWKError): key = jwk.HMACKey(hmac_key, 'RS512') with pytest.raises(JWKError): key = jwk.RSAKey(rsa_key, 'HS512') with pytest.raises(JWKError): key = jwk.ECKey(ec_key, 'RS512')
def test_invalid_jwk(self): with pytest.raises(JWKError): key = jwk.HMACKey(rsa_key, 'HS256') with pytest.raises(JWKError): key = jwk.RSAKey(hmac_key, 'RS256') with pytest.raises(JWKError): key = jwk.ECKey(rsa_key, 'ES256')
def gen_jose_rs256_key_pair(): """Generate RS256 asymmetric key pair for usage with python-jose Returns a tuple (private_key, public_key_jwk_dict) which can be used to sign (private_key) and verify (public_key_jwk_dict) tokens. Example usage: >>> private_key, public_key_jwk_dict = gen_jose_rs256_key_pair() >>> t = generate_token(key=private_key, algorithm="RS256") >>> claims = verify_and_decode_token(t, key=public_key_jwk_dict) >>> claims # doctest: +ELLIPSIS {...} """ key = rsa.generate_private_key( backend=crypto_default_backend(), public_exponent=65537, key_size=4096 ) private_key = key.private_bytes( encoding=crypto_serialization.Encoding.PEM, format=crypto_serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=crypto_serialization.NoEncryption(), ) public_key_string = ( key.public_key() .public_bytes( crypto_serialization.Encoding.PEM, crypto_serialization.PublicFormat.SubjectPublicKeyInfo, ) .decode("utf8") ) public_key_jwk_dict = jwk.RSAKey( algorithm=constants.Algorithms.RS256, key=public_key_string ).to_dict() return (private_key, public_key_jwk_dict)
def _get_pubkey(issuer, requested_kid): """ Construct URL to .well-known/openid-configuration. Fetch the config JSON document. By spec, it is required to contain a `jwks_uri` key. Use that URL to fetch the JWKS JSON document, and decode it. Return the resulting data structure. # From the spec (https://openid.net/specs/openid-connect-core-1_0.html, # section 10.1.1): The verifier knows to go back to the jwks_uri location to # re-retrieve the keys when it sees an unfamiliar kid value. """ # Try to read the data from the cache. The cache is a dictionary which is # thread-safe in CPython. The cache key is a combination of issuer and key # ID. That is, if there is just a single default key for an issuer (a key # without key ID) then we do not support rotation. A restart of the IAM is # required to pick that up. That is fine. try: return ISSUER_PUBLIC_KEYS[issuer][requested_kid] except KeyError: log.info('Public key for issuer `%s` with key ID `%s` not in cache', issuer, requested_kid) pass # Note(JP): while I am sure the spec says something about leading/trailing # slashes I opt for making sure to not miss a slash, and to not have a # double slash. cfg_url = issuer.rstrip('/') + '/.well-known/openid-configuration' verify_tls = True if config['TESTING']: verify_tls = False # Any one of the following three lines can raise an exception, as in case of # transport errors, unexpected HTTP response status codes, JSON # deserialiation problems, and failing key lookup. Handle of them in the # same way. try: response = requests.get(cfg_url, verify=verify_tls) response.raise_for_status() jwks_url = response.json()['jwks_uri'] except Exception as exc: log.error('Could not fetch jwks_uri from `%s`: %s', cfg_url, exc) raise falcon.HTTPInternalServerError( description='Could not fetch public key material from token issuer' ) # In terms of error-handling the same as above holds true. try: response = requests.get(jwks_url, verify=verify_tls) response.raise_for_status() jwks = response.json() except Exception as exc: log.error('Could not fetch jwks_uri from `%s`: %s', cfg_url, exc) raise falcon.HTTPInternalServerError( description='Could not fetch public key material from token issuer' ) # `jwks` is a Python data structure that is expected to represent a valid # JSON Web Key Set JSON document. # Note(JP): as of the JWKS spec it seems to be possible that a JWKS contains # more than one key where individual keys do not have a key ID. The keys can # then be distinguished by the key type. But even then.. to quote from RFC # 7517: "When "kid" values are used within a JWK Set, different keys within # the JWK Set SHOULD use distinct "kid" values. (One example in which # different keys might use the same "kid" value is if they have different # "kty" (key type) values but are considered to be equivalent alternatives # by the application using them.)" -- so, here, plan for the simplest cases: # if there is no `kid` set then treat it as 'default'. Only support RSA type # keys. If there is more than one RSA type key without key ID in the JWKS # then the last one wins. This will only be a problem with exotic providers, # it is known to not be a problem for Auth0. for key_dict in jwks['keys']: # `kid` is optional. Use 'default' as the key ID when it is not set. kid = key_dict.get('kid', 'default') # `kty` (the key type) is required. `alg` (the intended algorithm within # which this key is supposed to be used) is optional. Assume RSA and # RS256 for starters. We can widen that to support other key types, but # for Auth0 support we know that this is not required. kty = key_dict.get('kty', None) if kty != 'RSA': continue # The `prepared_key` attribute exposes the `cryptography`-native public # key object. key = jwk.RSAKey(key=key_dict, algorithm=jwk.ALGORITHMS.RS256).prepared_key log.info('Public key for issuer `%s` with key ID `%s` retrieved', issuer, requested_kid) # Insert key into cache. if issuer not in ISSUER_PUBLIC_KEYS: ISSUER_PUBLIC_KEYS[issuer] = dict() ISSUER_PUBLIC_KEYS[issuer][kid] = key try: return ISSUER_PUBLIC_KEYS[issuer][requested_kid] except KeyError: log.warning( 'Public key for issuer `%s` with key ID `%s` could not be obtained', issuer, requested_kid) # Let this exception terminate request handling, send an Internal Server # Error to the client. raise