diff --git a/docs/reference/google.auth.crypt.es256.rst b/docs/reference/google.auth.crypt.es256.rst new file mode 100644 index 000000000..5a6318482 --- /dev/null +++ b/docs/reference/google.auth.crypt.es256.rst @@ -0,0 +1,7 @@ +google.auth.crypt.es256 module +============================== + +.. automodule:: google.auth.crypt.es256 + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/reference/google.auth.crypt.rst b/docs/reference/google.auth.crypt.rst index 0833e7f2f..be142f428 100644 --- a/docs/reference/google.auth.crypt.rst +++ b/docs/reference/google.auth.crypt.rst @@ -12,4 +12,5 @@ Submodules .. toctree:: google.auth.crypt.base + google.auth.crypt.es256 google.auth.crypt.rsa diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 8dabaf9d6..89ad689a7 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,4 @@ +cryptography sphinx-docstring-typing urllib3 requests diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 0abe160a3..3877bff59 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -291,7 +291,21 @@ Impersonated :: target_credentials, target_audience=target_audience) -IDToken verification can be done for various type of IDTokens using the :class:`google.oauth2.id_token` module +IDToken verification can be done for various type of IDTokens using the +:class:`google.oauth2.id_token` module. It supports ID token signed with RS256 +and ES256 algorithms. However, ES256 algorithm won't be available unless +`cryptography` dependency of version at least 1.4.0 is installed. You can check +the dependency with `pip freeze` or try `from google.auth.crypt import es256`. +The following is an example of verifying ID tokens: + + from google.auth2 import id_token + + request = google.auth.transport.requests.Request() + + try: + decoded_token = id_token.verify_token(token_to_verify,request) + except ValueError: + # Verification failed. A sample end-to-end flow using an ID Token against a Cloud Run endpoint maybe :: diff --git a/google/auth/crypt/__init__.py b/google/auth/crypt/__init__.py index 39929fa0a..15ac95068 100644 --- a/google/auth/crypt/__init__.py +++ b/google/auth/crypt/__init__.py @@ -31,6 +31,10 @@ private_key = open('private_key.pem').read() signer = crypt.RSASigner.from_string(private_key) signature = signer.sign(message) + +The code above also works for :class:`ES256Signer` and :class:`ES256Verifier`. +Note that these two classes are only available if your `cryptography` dependency +version is at least 1.4.0. """ import six @@ -38,8 +42,23 @@ from google.auth.crypt import base from google.auth.crypt import rsa +try: + from google.auth.crypt import es256 +except ImportError: # pragma: NO COVER + es256 = None + +if es256 is not None: # pragma: NO COVER + __all__ = [ + "ES256Signer", + "ES256Verifier", + "RSASigner", + "RSAVerifier", + "Signer", + "Verifier", + ] +else: # pragma: NO COVER + __all__ = ["RSASigner", "RSAVerifier", "Signer", "Verifier"] -__all__ = ["RSASigner", "RSAVerifier", "Signer", "Verifier"] # Aliases to maintain the v1.0.0 interface, as the crypt module was split # into submodules. @@ -48,9 +67,13 @@ RSASigner = rsa.RSASigner RSAVerifier = rsa.RSAVerifier +if es256 is not None: # pragma: NO COVER + ES256Signer = es256.ES256Signer + ES256Verifier = es256.ES256Verifier + -def verify_signature(message, signature, certs): - """Verify an RSA cryptographic signature. +def verify_signature(message, signature, certs, verifier_cls=rsa.RSAVerifier): + """Verify an RSA or ECDSA cryptographic signature. Checks that the provided ``signature`` was generated from ``bytes`` using the private key associated with the ``cert``. @@ -60,6 +83,9 @@ def verify_signature(message, signature, certs): signature (Union[str, bytes]): The cryptographic signature to check. certs (Union[Sequence, str, bytes]): The certificate or certificates to use to check the signature. + verifier_cls (Optional[~google.auth.crypt.base.Signer]): Which verifier + class to use for verification. This can be used to select different + algorithms, such as RSA or ECDSA. Default value is :class:`RSAVerifier`. Returns: bool: True if the signature is valid, otherwise False. @@ -68,7 +94,7 @@ def verify_signature(message, signature, certs): certs = [certs] for cert in certs: - verifier = rsa.RSAVerifier.from_string(cert) + verifier = verifier_cls.from_string(cert) if verifier.verify(message, signature): return True return False diff --git a/google/auth/crypt/es256.py b/google/auth/crypt/es256.py new file mode 100644 index 000000000..5bfd57fb8 --- /dev/null +++ b/google/auth/crypt/es256.py @@ -0,0 +1,145 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ECDSA (ES256) verifier and signer that use the ``cryptography`` library. +""" + +import cryptography.exceptions +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import padding +import cryptography.x509 +import pkg_resources + +from google.auth import _helpers +from google.auth.crypt import base + +_IMPORT_ERROR_MSG = ( + "cryptography>=1.4.0 is required to use cryptography-based ECDSA " "algorithms" +) + +try: # pragma: NO COVER + release = pkg_resources.get_distribution("cryptography").parsed_version + if release < pkg_resources.parse_version("1.4.0"): + raise ImportError(_IMPORT_ERROR_MSG) +except pkg_resources.DistributionNotFound: # pragma: NO COVER + raise ImportError(_IMPORT_ERROR_MSG) + + +_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----" +_BACKEND = backends.default_backend() +_PADDING = padding.PKCS1v15() + + +class ES256Verifier(base.Verifier): + """Verifies ECDSA cryptographic signatures using public keys. + + Args: + public_key ( + cryptography.hazmat.primitives.asymmetric.ec.ECDSAPublicKey): + The public key used to verify signatures. + """ + + def __init__(self, public_key): + self._pubkey = public_key + + @_helpers.copy_docstring(base.Verifier) + def verify(self, message, signature): + message = _helpers.to_bytes(message) + try: + self._pubkey.verify(signature, message, ec.ECDSA(hashes.SHA256())) + return True + except (ValueError, cryptography.exceptions.InvalidSignature): + return False + + @classmethod + def from_string(cls, public_key): + """Construct an Verifier instance from a public key or public + certificate string. + + Args: + public_key (Union[str, bytes]): The public key in PEM format or the + x509 public key certificate. + + Returns: + Verifier: The constructed verifier. + + Raises: + ValueError: If the public key can't be parsed. + """ + public_key_data = _helpers.to_bytes(public_key) + + if _CERTIFICATE_MARKER in public_key_data: + cert = cryptography.x509.load_pem_x509_certificate( + public_key_data, _BACKEND + ) + pubkey = cert.public_key() + + else: + pubkey = serialization.load_pem_public_key(public_key_data, _BACKEND) + + return cls(pubkey) + + +class ES256Signer(base.Signer, base.FromServiceAccountMixin): + """Signs messages with an ECDSA private key. + + Args: + private_key ( + cryptography.hazmat.primitives.asymmetric.ec.ECDSAPrivateKey): + The private key to sign with. + key_id (str): Optional key ID used to identify this private key. This + can be useful to associate the private key with its associated + public key or certificate. + """ + + def __init__(self, private_key, key_id=None): + self._key = private_key + self._key_id = key_id + + @property + @_helpers.copy_docstring(base.Signer) + def key_id(self): + return self._key_id + + @_helpers.copy_docstring(base.Signer) + def sign(self, message): + message = _helpers.to_bytes(message) + return self._key.sign(message, ec.ECDSA(hashes.SHA256())) + + @classmethod + def from_string(cls, key, key_id=None): + """Construct a RSASigner from a private key in PEM format. + + Args: + key (Union[bytes, str]): Private key in PEM format. + key_id (str): An optional key id used to identify the private key. + + Returns: + google.auth.crypt._cryptography_rsa.RSASigner: The + constructed signer. + + Raises: + ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode). + UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded + into a UTF-8 ``str``. + ValueError: If ``cryptography`` "Could not deserialize key data." + """ + key = _helpers.to_bytes(key) + private_key = serialization.load_pem_private_key( + key, password=None, backend=_BACKEND + ) + return cls(private_key, key_id=key_id) diff --git a/google/auth/jwt.py b/google/auth/jwt.py index cdd69ac8a..9248eb27f 100644 --- a/google/auth/jwt.py +++ b/google/auth/jwt.py @@ -59,8 +59,18 @@ from google.auth import exceptions import google.auth.credentials +try: + from google.auth.crypt import es256 +except ImportError: # pragma: NO COVER + es256 = None + _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds _DEFAULT_MAX_CACHE_SIZE = 10 +_ALGORITHM_TO_VERIFIER_CLASS = {"RS256": crypt.RSAVerifier} +_CRYPTOGRAPHY_BASED_ALGORITHMS = set(["ES256"]) + +if es256 is not None: # pragma: NO COVER + _ALGORITHM_TO_VERIFIER_CLASS["ES256"] = es256.ES256Verifier def encode(signer, payload, header=None, key_id=None): @@ -83,7 +93,12 @@ def encode(signer, payload, header=None, key_id=None): if key_id is None: key_id = signer.key_id - header.update({"typ": "JWT", "alg": "RS256"}) + header.update({"typ": "JWT"}) + + if es256 is not None and isinstance(signer, es256.ES256Signer): + header.update({"alg": "ES256"}) + else: + header.update({"alg": "RS256"}) if key_id is not None: header["kid"] = key_id @@ -217,10 +232,30 @@ def decode(token, certs=None, verify=True, audience=None): if not verify: return payload + # Pluck the key id and algorithm from the header and make sure we have + # a verifier that can support it. + key_alg = header.get("alg") + key_id = header.get("kid") + + try: + verifier_cls = _ALGORITHM_TO_VERIFIER_CLASS[key_alg] + except KeyError as exc: + if key_alg in _CRYPTOGRAPHY_BASED_ALGORITHMS: + six.raise_from( + ValueError( + "The key algorithm {} requires the cryptography package " + "to be installed.".format(key_alg) + ), + exc, + ) + else: + six.raise_from( + ValueError("Unsupported signature algorithm {}".format(key_alg)), exc + ) + # If certs is specified as a dictionary of key IDs to certificates, then # use the certificate identified by the key ID in the token header. if isinstance(certs, Mapping): - key_id = header.get("kid") if key_id: if key_id not in certs: raise ValueError("Certificate for key id {} not found.".format(key_id)) @@ -232,7 +267,9 @@ def decode(token, certs=None, verify=True, audience=None): certs_to_check = certs # Verify that the signature matches the message. - if not crypt.verify_signature(signed_section, signature, certs_to_check): + if not crypt.verify_signature( + signed_section, signature, certs_to_check, verifier_cls + ): raise ValueError("Could not verify token signature.") # Verify the issued at and created times in the payload. diff --git a/tests/crypt/test_es256.py b/tests/crypt/test_es256.py new file mode 100644 index 000000000..087ce6e23 --- /dev/null +++ b/tests/crypt/test_es256.py @@ -0,0 +1,131 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os + +from cryptography.hazmat.primitives.asymmetric import ec +import pytest + +from google.auth import _helpers +from google.auth.crypt import base +from google.auth.crypt import es256 + + +DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") + +# To generate es256_privatekey.pem, es256_privatekey.pub, and +# es256_public_cert.pem: +# $ openssl ecparam -genkey -name prime256v1 -noout -out es256_privatekey.pem +# $ openssl ec -in es256-private-key.pem -pubout -out es256-publickey.pem +# $ openssl req -new -x509 -key es256_privatekey.pem -out \ +# > es256_public_cert.pem + +with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh: + PRIVATE_KEY_BYTES = fh.read() + PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES + +with open(os.path.join(DATA_DIR, "es256_publickey.pem"), "rb") as fh: + PUBLIC_KEY_BYTES = fh.read() + +with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh: + PUBLIC_CERT_BYTES = fh.read() + +SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "es256_service_account.json") + +with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh: + SERVICE_ACCOUNT_INFO = json.load(fh) + + +class TestES256Verifier(object): + def test_verify_success(self): + to_sign = b"foo" + signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES) + actual_signature = signer.sign(to_sign) + + verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) + assert verifier.verify(to_sign, actual_signature) + + def test_verify_unicode_success(self): + to_sign = u"foo" + signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES) + actual_signature = signer.sign(to_sign) + + verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) + assert verifier.verify(to_sign, actual_signature) + + def test_verify_failure(self): + verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) + bad_signature1 = b"" + assert not verifier.verify(b"foo", bad_signature1) + bad_signature2 = b"a" + assert not verifier.verify(b"foo", bad_signature2) + + def test_from_string_pub_key(self): + verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) + assert isinstance(verifier, es256.ES256Verifier) + assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) + + def test_from_string_pub_key_unicode(self): + public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) + verifier = es256.ES256Verifier.from_string(public_key) + assert isinstance(verifier, es256.ES256Verifier) + assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) + + def test_from_string_pub_cert(self): + verifier = es256.ES256Verifier.from_string(PUBLIC_CERT_BYTES) + assert isinstance(verifier, es256.ES256Verifier) + assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) + + def test_from_string_pub_cert_unicode(self): + public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) + verifier = es256.ES256Verifier.from_string(public_cert) + assert isinstance(verifier, es256.ES256Verifier) + assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) + + +class TestES256Signer(object): + def test_from_string_pkcs1(self): + signer = es256.ES256Signer.from_string(PKCS1_KEY_BYTES) + assert isinstance(signer, es256.ES256Signer) + assert isinstance(signer._key, ec.EllipticCurvePrivateKey) + + def test_from_string_pkcs1_unicode(self): + key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) + signer = es256.ES256Signer.from_string(key_bytes) + assert isinstance(signer, es256.ES256Signer) + assert isinstance(signer._key, ec.EllipticCurvePrivateKey) + + def test_from_string_bogus_key(self): + key_bytes = "bogus-key" + with pytest.raises(ValueError): + es256.ES256Signer.from_string(key_bytes) + + def test_from_service_account_info(self): + signer = es256.ES256Signer.from_service_account_info(SERVICE_ACCOUNT_INFO) + + assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] + assert isinstance(signer._key, ec.EllipticCurvePrivateKey) + + def test_from_service_account_info_missing_key(self): + with pytest.raises(ValueError) as excinfo: + es256.ES256Signer.from_service_account_info({}) + + assert excinfo.match(base._JSON_FILE_PRIVATE_KEY) + + def test_from_service_account_file(self): + signer = es256.ES256Signer.from_service_account_file(SERVICE_ACCOUNT_JSON_FILE) + + assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] + assert isinstance(signer._key, ec.EllipticCurvePrivateKey) diff --git a/tests/data/es256_privatekey.pem b/tests/data/es256_privatekey.pem new file mode 100644 index 000000000..5c950b514 --- /dev/null +++ b/tests/data/es256_privatekey.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49 +AwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ +z2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA== +-----END EC PRIVATE KEY----- diff --git a/tests/data/es256_public_cert.pem b/tests/data/es256_public_cert.pem new file mode 100644 index 000000000..774ca1484 --- /dev/null +++ b/tests/data/es256_public_cert.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE----- +MIIBGDCBwAIJAPUA0H4EQWsdMAoGCCqGSM49BAMCMBUxEzARBgNVBAMMCnVuaXQt +dGVzdHMwHhcNMTkwNTA5MDI1MDExWhcNMTkwNjA4MDI1MDExWjAVMRMwEQYDVQQD +DAp1bml0LXRlc3RzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp21 +6OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGC +fj+b1IDIoDAKBggqhkjOPQQDAgNHADBEAh8PcDTMyWk8SHqV/v8FLuMbDxdtAsq2 +dwCpuHQwqCcmAiEAnwtkiyieN+8zozaf1P4QKp2mAqNGqua50y3ua5uVotc= +-----END CERTIFICATE----- diff --git a/tests/data/es256_publickey.pem b/tests/data/es256_publickey.pem new file mode 100644 index 000000000..51f2a03fa --- /dev/null +++ b/tests/data/es256_publickey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp216OCFm73C8W/VRHZW +cO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA== +-----END PUBLIC KEY----- diff --git a/tests/data/es256_service_account.json b/tests/data/es256_service_account.json new file mode 100644 index 000000000..dd26719f6 --- /dev/null +++ b/tests/data/es256_service_account.json @@ -0,0 +1,10 @@ +{ + "type": "service_account", + "project_id": "example-project", + "private_key_id": "1", + "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49\nAwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ\nz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==\n-----END EC PRIVATE KEY-----", + "client_email": "service-account@example.com", + "client_id": "1234", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token" +} diff --git a/tests/test_jwt.py b/tests/test_jwt.py index b0c6e48e9..488aee467 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -37,6 +37,12 @@ with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh: OTHER_CERT_BYTES = fh.read() +with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh: + EC_PRIVATE_KEY_BYTES = fh.read() + +with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh: + EC_PUBLIC_CERT_BYTES = fh.read() + SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh: @@ -68,8 +74,21 @@ def test_encode_extra_headers(signer): @pytest.fixture -def token_factory(signer): - def factory(claims=None, key_id=None): +def es256_signer(): + return crypt.ES256Signer.from_string(EC_PRIVATE_KEY_BYTES, "1") + + +def test_encode_basic_es256(es256_signer): + test_payload = {"test": "value"} + encoded = jwt.encode(es256_signer, test_payload) + header, payload, _, _ = jwt._unverified_decode(encoded) + assert payload == test_payload + assert header == {"typ": "JWT", "alg": "ES256", "kid": es256_signer.key_id} + + +@pytest.fixture +def token_factory(signer, es256_signer): + def factory(claims=None, key_id=None, use_es256_signer=False): now = _helpers.datetime_to_secs(_helpers.utcnow()) payload = { "aud": "audience@example.com", @@ -86,7 +105,10 @@ def factory(claims=None, key_id=None): signer._key_id = None key_id = None - return jwt.encode(signer, payload, key_id=key_id) + if use_es256_signer: + return jwt.encode(es256_signer, payload, key_id=key_id) + else: + return jwt.encode(signer, payload, key_id=key_id) return factory @@ -98,6 +120,15 @@ def test_decode_valid(token_factory): assert payload["metadata"]["meta"] == "data" +def test_decode_valid_es256(token_factory): + payload = jwt.decode( + token_factory(use_es256_signer=True), certs=EC_PUBLIC_CERT_BYTES + ) + assert payload["aud"] == "audience@example.com" + assert payload["user"] == "billy bob" + assert payload["metadata"]["meta"] == "data" + + def test_decode_valid_with_audience(token_factory): payload = jwt.decode( token_factory(), certs=PUBLIC_CERT_BYTES, audience="audience@example.com" @@ -201,6 +232,29 @@ def test_decode_no_key_id(token_factory): assert payload["user"] == "billy bob" +def test_decode_unknown_alg(): + headers = json.dumps({u"kid": u"1", u"alg": u"fakealg"}) + token = b".".join( + map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"]) + ) + + with pytest.raises(ValueError) as excinfo: + jwt.decode(token) + assert excinfo.match(r"fakealg") + + +def test_decode_missing_crytography_alg(monkeypatch): + monkeypatch.delitem(jwt._ALGORITHM_TO_VERIFIER_CLASS, "ES256") + headers = json.dumps({u"kid": u"1", u"alg": u"ES256"}) + token = b".".join( + map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"]) + ) + + with pytest.raises(ValueError) as excinfo: + jwt.decode(token) + assert excinfo.match(r"cryptography") + + def test_roundtrip_explicit_key_id(token_factory): token = token_factory(key_id="3") certs = {"2": OTHER_CERT_BYTES, "3": PUBLIC_CERT_BYTES}