Repository: mpdavis/python-jose

Issue:
Add message about lack of X.509 certificate support in documentation
I get this error when using algorithms='RS256' on google app engine.

Full stack trace
```
Traceback (most recent call last):
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/tools/devappserver2/python/request_handler.py", line 226, in handle_interactive_request
    exec(compiled_code, self._command_globals)
  File "<string>", line 12, in <module>
  File "lib/jose/jwt.py", line 121, in decode
    payload = jws.verify(token, key, algorithms, verify=verify_signature)
  File "lib/jose/jws.py", line 75, in verify
    _verify_signature(signing_input, header, signature, key, algorithms)
  File "lib/jose/jws.py", line 218, in _verify_signature
    key = jwk.construct(key, alg)
  File "lib/jose/jwk.py", line 65, in construct
    return RSAKey(key_data, algorithm)
  File "lib/jose/jwk.py", line 201, in __init__
    raise JWKError(e)
JWKError: RSA key format is not supported
```

Discussion:
codecov-io: ## [Current coverage](https://codecov.io/gh/mpdavis/python-jose/pull/33?src=pr) is 95.60% (diff: 93.10%)
> Merging [#33](https://codecov.io/gh/mpdavis/python-jose/pull/33?src=pr) into [master](https://codecov.io/gh/mpdavis/python-jose/branch/master?src=pr) will decrease coverage by **0.18%**

```diff
@@             master        #33   diff @@
==========================================
  Files             7          7          
  Lines           498        523    +25   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            477        500    +23   
- Misses           21         23     +2   
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [00827f6...b1cc5f0](https://codecov.io/gh/mpdavis/python-jose/compare/00827f66c371f40d3295d44bfa889f25ec2ca72f...b1cc5f01ec7b7b8f7b33306b628c6c32daaf587d?src=pr)

Patch:
diff --git a/jose/jwk.py b/jose/jwk.py
index 21398e4..4f69f51 100644
--- a/jose/jwk.py
+++ b/jose/jwk.py
@@ -12,6 +12,7 @@ import Crypto.Hash.SHA512
 
 from Crypto.PublicKey import RSA
 from Crypto.Signature import PKCS1_v1_5
+from Crypto.Util.asn1 import DerSequence
 
 import ecdsa
 
@@ -188,18 +189,24 @@ class RSAKey(Key):
             return
 
         if isinstance(key, dict):
-            self.prepared_key = self._process_jwk(key)
+            self._process_jwk(key)
             return
 
         if isinstance(key, six.string_types):
             if isinstance(key, six.text_type):
                 key = key.encode('utf-8')
 
+            if key.startswith(b'-----BEGIN CERTIFICATE-----'):
+                try:
+                    self._process_cert(key)
+                except Exception as e:
+                    raise JWKError(e)
+                return
+
             try:
                 self.prepared_key = RSA.importKey(key)
             except Exception as e:
                 raise JWKError(e)
-
             return
 
         raise JWKError('Unable to parse an RSA_JWK from key: %s' % key)
@@ -214,6 +221,16 @@ class RSAKey(Key):
         self.prepared_key = RSA.construct((n, e))
         return self.prepared_key
 
+    def _process_cert(self, key):
+        pemLines = key.replace(b' ', b'').split()
+        certDer = base64url_decode(b''.join(pemLines[1:-1]))
+        certSeq = DerSequence()
+        certSeq.decode(certDer)
+        tbsSeq = DerSequence()
+        tbsSeq.decode(certSeq[0])
+        self.prepared_key = RSA.importKey(tbsSeq[6])
+        return
+
     def sign(self, msg):
         try:
             return PKCS1_v1_5.new(self.prepared_key).sign(self.hash_alg.new(msg))
diff --git a/jose/jws.py b/jose/jws.py
index dc6ad5c..7d49a4c 100644
--- a/jose/jws.py
+++ b/jose/jws.py
@@ -214,15 +214,34 @@ def _sig_matches_keys(keys, signing_input, signature, alg):
 
 
 def _get_keys(key):
-    if 'keys' in key:  # JWK Set per RFC 7517
-        if not isinstance(key, Mapping):  # Caller didn't JSON-decode
-            key = json.loads(key)
+
+    try:
+        key = json.loads(key)
+    except Exception:
+        pass
+
+    # JWK Set per RFC 7517
+    if 'keys' in key:
         return key['keys']
+
+    # Individual JWK per RFC 7517
+    elif 'kty' in key:
+        return (key,)
+
+    # Some other mapping. Firebase uses just dict of kid, cert pairs
+    elif isinstance(key, Mapping):
+        values = key.values()
+        if values:
+            return values
+        return (key,)
+
     # Iterable but not text or mapping => list- or tuple-like
     elif (isinstance(key, Iterable) and
           not (isinstance(key, six.string_types) or isinstance(key, Mapping))):
         return key
-    else:  # Scalar value, wrap in tuple.
+
+    # Scalar value, wrap in tuple.
+    else:
         return (key,)
 
 


Test patch:
diff --git a/tests/algorithms/test_RSA.py b/tests/algorithms/test_RSA.py
index 026674f..ae45c63 100644
--- a/tests/algorithms/test_RSA.py
+++ b/tests/algorithms/test_RSA.py
@@ -85,3 +85,8 @@ class TestRSAAlgorithm:
         key = object()
         with pytest.raises(JOSEError):
             RSAKey(key, ALGORITHMS.RS256)
+
+    def test_bad_cert(self):
+        key = '-----BEGIN CERTIFICATE-----'
+        with pytest.raises(JOSEError):
+            RSAKey(key, ALGORITHMS.RS256)
diff --git a/tests/test_firebase.py b/tests/test_firebase.py
new file mode 100644
index 0000000..041739b
--- /dev/null
+++ b/tests/test_firebase.py
@@ -0,0 +1,42 @@
+
+import json
+
+from jose import jwt
+
+firebase_certs = {
+    "6f83ab6e516e718fba9ddeb6647fd5fb752a151b": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIP5V2bjX2bXUwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYw\nODMxMDA0NTI2WhcNMTYwOTAzMDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAKHHtOMXBD+0YTtZHuzFrERiiwa+D6Ybq4SUHlicgRPV3Uk2\nvnTOqg1EhxshEXqjkAQbbRop9hhHTc+p8rBxgYGuLcZsBhGrnRqU6FnTTiWB1x5V\nvOfCkPE60W07gi8p+HyB8cqw1Tz2LnRUw/15888CrspVeumtNUkhXSRKzeS2BI4l\nkuOMkqmsMSu1yB5IZm5meMyta1uhJnP93jKmdar19RkZXOlFcT+fsSY2FPuqvDvX\nssChgZgNV5qtk0CIzexmFJaUFzpKE/RxqdIJooB1H83fUBGVK+9v3Ko+BI+GEvUc\nxIGAEWu2KrbjwPNzzC3/UV9aSfHEOJxQoutPviECAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAIHOiqxXm1IcuXE87ELyKYDG0/gZPzCHz98h/x0LExrs\nd0bOYOIA08rt6qllmP24oT3hQt86HmDb932pm/fjaLL68x81TjYq6cFO0JxOzts+\nY+9XxkdP8Qu7UJ8Dx+rRvDN1MUxLTvBVXdamhkhDusx7PB5kK1ixWtf91qrl/J9e\nUYQBnJ4E9wI8U5HVkW3IBWvsFt/+gMO1EcoNBdB2cY/4N3l3oxm5PSNDS4DTEs2f\nAYZDqo6PJt2tTRGSmvLBKSCqcT7eWBbIwBht3Uw8CvOMbVYGBWjbFeua3Q3fe+p7\n7UbFOLIvSGR516kyZqxy9pLoA9+2TvbpYwWu6mLCZtg=\n-----END CERTIFICATE-----\n",
+    "fc2da7fa53d92e3bcba8a17e74b34da9dd585065": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIINfZYQW9uekMwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYw\nODI5MDA0NTI2WhcNMTYwOTAxMDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMvfJ5DY7lV4txW0zn9ayMxwAp5BzUhyIbuZkmsmMLRrNl+i\nid4lawojB846YtcTPZLD/5QpXRumAAUI5NA023fxaUdriM25zewpSnZWs6eUf0O6\nONES8Xk4WD2fbyPz6cgnsFEfMslNd3NypRiB9fVG6LFj6TFHC64o/YEeQB2dwkJZ\nXknKSEkFJSRC83TiHUlWzaRjmTdGRrvGEWHxr+xJltP8tPPlJUKu2VadgMbGlkKU\n5dBRhvWwZZW0zJupuKzd27O2lPkxfbx9vrUbsfqZcN4OY5Xg+ijQJVTv0/qcplsd\nPZ9Uui0QsBOPbrIO+5/Tq9FIBqxzUlpWwetv6pMCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBALqWwzIQSK94hxTmxlA+RoyMvb8fyTcECM2qY+n+PDb5\nMvt8zqM6AwGjK1hvcUg08BEsnqqRqC81dkSEReS9KCoTY/oQ0sCCpwL3QP3puoxp\nfZU9CSwvnrFTJjC2Q/b8BlWta4CSDwpxpy/K3wm6tRn5ED4rPcP4FRqWU5jyHiug\nRrNkKiG7TeBBvQ3ZlF9K4JSx1yn9g7EvPBcmygop5FIKI1uS+URxeyavtlwfnTTs\nDtRVV/x0LDkHoJ2Agy7l2MqT7eoRKh5VNucQONLrcZT1AY02eZi/WVSjgpzC48eP\nV9xlcgIaRbS/JDULYgW5h0uVdRNqSVGJ6yBLXT2uaBA=\n-----END CERTIFICATE-----\n",
+    "8226146523a1b8894ba03ad525667b9475d393f5": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIWAKW/IRYcAwwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYw\nODMwMDA0NTI2WhcNMTYwOTAyMDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALJAt+ws+XNdDnDSYFp0YnQ5e8QqfMFrwp1l1r/mNSUF840I\nsbm50Z89aNpQgFOsORS/TYyHOeasiBhsJ5HWmfxo0PBTFifKI/OedLlltxZZCHa+\nEO/75Fbeydexokvfq6thT7C+xL45kJzbvKKNAw4WCAW6vwzyz+d/IrWCs9Iqa2ZX\nSiKnMPzPxZj6s+AhHPVxsR8dBMZ+NdK/wh9OcPWjLAxLEWBvd0Gp315bIVjVc9pV\neYcTapu/s4DSwgz4twovAyUziwsa+HJ+2FFNDZExf/XQUVBW5le8gGEdfl3kW1yu\nzdO6e1LwVTDAXULydPBL5lb6vTX2/ICmMzHXzIUCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAHyACbK1WfP9WspLfxvgJaNvwvygnP6cggLMvqq/dRxP\nlemvxfVaHK19sIXI6p0H4RBjI9FID5otzuyV54p1LBKgLIMTWcMYdL0wieeBg4Ud\nwgLEutIERpJU5oRMpSuZZYW75d0o+U1qOEhDswliqW1xofxNjRgNyrOYc6hMJzIS\ng9U4C4fplT/m3x5uQNjfzN/0CxfQf54WaD15w1lPGQAMJSWQDaxDTi41bW0Jwp4N\ndshOVn+btUUwL5TXDKaVkg1IHfG57FwvPJ5hKs4pbP5SIm+Sc1utIMMTBsRDRJVK\nyHaB5Bj9KcpQk7FvdT/KtzetPowhnxu9ow+KJcnP+7w=\n-----END CERTIFICATE-----\n",
+    "dd694b16c1b0ce31878a72dfa6c0cd4db3dd7edf": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIffru9igojE4wDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYw\nOTAxMDA0NTI2WhcNMTYwOTA0MDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALaxpG4i7EgYpzaJsykaZzKmTTnm+kIPJBKb6t41ByUWt7J+\nnoUmlMiAVkXj7GAmc3usroJdYNZ8iMSpAWsIMgg7HLrqv/hMDY6+33rCqsvXD2tF\nCtJbRKzSMKu+AIc1uirkX3L3aHfKRzFbsr+8JqOigY3sVAb42FeATVHB0uCRyoE5\nfqxbt8nIPCFR/lFP51L0Wf5hGIH5kHJEuXx/7GOUQPN196P3sRI9jLv6nrWqGTAR\nVhuY9KXRz0jlVQeKZV5mWstcIXgxn2MfzfoHx4nuSNknJdrfHNp0r2XPf9Fre7Jd\n73slrVUwL2VWyZJdIBxJuYz2QjEQLzz+eJGyWcMCAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAFTpRr9/cEkFHSbP5c7gr926kSxe1e9u9JjzR7l9Zv5l\nfskkLxIZcGlx/FoccWCwDPYl2Nh0Pr++TJ2hWVe/LpjppUxl4QVqfbVzyJezn2UR\nhLnGASQ0ckPoNTJhxjA6PVGtyXWB67oCDEgz/Pl6jjKEMtilyXh93rBmOpt6jq9e\nlwiZaa5wTUwIhHI972rLveYkssVkspmp4RIWHoh1nxUjYPMtcTCf9GFjEMLNdDBj\nYldCEzL34V60ObBSkzV3Zx7UNwoa80+SEJc9gQsBHVJbjXl7V9ODL52OHnciiEA8\n+d/xy2tBzdCD5EUR3aaYZYqQ16VV6LeU8FoxFn6/nxw=\n-----END CERTIFICATE-----\n",
+    "f4b0a5c73ad85a5da09f0e7f76463631339e0bbf": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIWDhBeVUilCcwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTYw\nNzAxMDA0NTI2WhcNMTYwNzA0MDExNTI2WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALRWaRmoi5EFyj5TBrUGKFI6uBJ4x9wSHq9tlRL1qmnwzdNb\nlDoeoh6Gw3H54IqM0XqjZZwgV5KXOQDOaoUpMBRH93x7Ma7NjhiDtpQr0JSbFIQL\nsIay/VxQ9gfa/I83HViEAbF1FXjhBKniwFKUv26mU30upZfsDQkHM8OLc/iXRvhA\nYn7S732Oefdv0kJ9t3h+WOGKGVkYfDaAGn5Uyzx+9oyyLY33borKOBBzphSQlZCr\nL569zTXvvLgvdStrsPGaiRGj64DGXD6LCg6acLJcMUvlVUO6THHJHVgp8pzlrPQG\n3B1rZk61lZqJyjK/nTi2tY9GPLfdxOfDAMjNoz8CAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAIlFwO3C+X2+na0nLjR+zQYGHzZYqFe4V67P6ugFJxun\nxP8pyDCYAGer1mkDcIyDacdQ3natNp0xv61a0yk5tSmDYZbXZRTFdLkf/GzH+VmH\nEMl5W4TvxjAe/x2opm3QUaPC+jVlvndcP99FF5ULFp7/PwSTp8uzyrd/fhSFaxhq\nuIW4syNzDSpDItzUsiKCtsKGYX/qvd/cNP8cXlPd5rWTM4Sic9Baf2nXuHaZRkBr\nSJYcxdh8xbGsY1tC8TIgWot6GXtldNvXDLqRUwb2t6Rr3Tqhbc0CcHndTCuHXf0i\n0s9jU/UCrNhhmaD0rZLHQ2tuN6W/xpOHKtO0a8Lys7c=\n-----END CERTIFICATE-----\n"
+}
+
+firebase_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImY0YjBhNWM3M2FkODVhNWRhMDlmMGU3Zjc2NDYzNjMxMzM5ZTBiYmYifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd2Vkb3RyYW5zZmVyLTIwMTYiLCJhdWQiOiJ3ZWRvdHJhbnNmZXItMjAxNiIsImF1dGhfdGltZSI6MTQ2NzM0NjI3MCwidXNlcl9pZCI6IjRjemVXVllIekNNVnN0WEZOYldHVXBKYmJTZzEiLCJzdWIiOiI0Y3plV1ZZSHpDTVZzdFhGTmJXR1VwSmJiU2cxIiwiaWF0IjoxNDY3MzQ2MjcwLCJleHAiOjE0NjczNDk4NzAsImVtYWlsIjoic2V1bkBjbXUuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7InBhc3N3b3JkIjpbInNldW5AY211LmNvbSJdLCJlbWFpbCI6WyJzZXVuQGNtdS5jb20iXX19fQ.U-fYjx8rMm5tYV24r0uEcNQtIe3UKULxsHecLdGzTbi1v-VKzKDk_QPL26SPDoU8JUMY3nJQ1hOE9AapBrQck8NVUZSKFMD49XdtsyoN2kKdinpFR1hSxIE0L2dRStS7OZ8sGiX866lNa52Cr6TXSsnMD6N2P0OtVE5EeD1Nf-AiJ-gsaLrP4tBnmj1MNYhEYVHb6sAUrT3nEI9gWmeKcPWPfn76FGTdGWZ2mjdaeAG4RbuFL4cHdOISA_0HVLGJxuNyEHAHybDX8mVdNW_F4yzL3H-SmPFY5Kv3tCdBzpzhUKfNOnFFmf2ggFOJnDsqMp-TZaIPk6ce_ltqhQ0dnQ"
+
+
+class TestFirebase:
+
+    def test_individual_cert(self):
+        jwt.decode(
+            firebase_token,
+            firebase_certs["f4b0a5c73ad85a5da09f0e7f76463631339e0bbf"],
+            algorithms='RS256',
+            options={'verify_exp': False, 'verify_aud': False}
+        )
+
+    def test_certs_dict(self):
+        jwt.decode(
+            firebase_token,
+            firebase_certs,
+            algorithms='RS256',
+            options={'verify_exp': False, 'verify_aud': False}
+        )
+
+    def test_certs_string(self):
+        certs = json.dumps(firebase_certs)
+        jwt.decode(
+            firebase_token,
+            certs,
+            algorithms='RS256',
+            options={'verify_exp': False, 'verify_aud': False}
+        )
diff --git a/tests/test_jws.py b/tests/test_jws.py
index a57c7f4..7cc2bc2 100644
--- a/tests/test_jws.py
+++ b/tests/test_jws.py
@@ -213,6 +213,10 @@ class TestGetKeys(object):
         key = '{"keys": [{}, {}]}'
         assert [{}, {}] == jws._get_keys(key)
 
+    def test_RFC7517_jwk(self):
+        key = {'kty': 'hsa', 'k': 'secret', 'alg': 'HS256', 'use': 'sig'}
+        assert (key, ) == jws._get_keys(key)
+
     def test_RFC7517_mapping(self):
         key = {"keys": [{}, {}]}
         assert [{}, {}] == jws._get_keys(key)