diff --git a/autopush/main.py b/autopush/main.py index e3bb82a3..8a8a0fdf 100644 --- a/autopush/main.py +++ b/autopush/main.py @@ -208,6 +208,7 @@ def from_argparse(cls, ns): cors=not ns.no_cors, bear_hash_key=ns.auth_key, proxy_protocol_port=ns.proxy_protocol_port, + use_cryptography=ns.use_cryptography, ) @@ -281,4 +282,5 @@ def from_argparse(cls, ns): auto_ping_timeout=ns.auto_ping_timeout, max_connections=ns.max_connections, close_handshake_timeout=ns.close_handshake_timeout, + use_cryptography=ns.use_cryptography, ) diff --git a/autopush/main_argparse.py b/autopush/main_argparse.py index 6d59e667..6416e799 100644 --- a/autopush/main_argparse.py +++ b/autopush/main_argparse.py @@ -99,6 +99,10 @@ def add_shared_args(parser): help="Enable the debug _memusage API on Port", type=int, default=None, env_var='MEMUSAGE_PORT') + parser.add_argument('--use_cryptography', + help="Use the cryptography library vs. JOSE", + action="store_true", + default=False, env_var="USE_CRYPTOGRAPHY") # No ENV because this is for humans _add_external_router_args(parser) _obsolete_args(parser) diff --git a/autopush/settings.py b/autopush/settings.py index d073d2cb..5e30ff9b 100644 --- a/autopush/settings.py +++ b/autopush/settings.py @@ -127,6 +127,9 @@ class AutopushSettings(object): # generate legacy data. _notification_legacy = attrib(default=False) # type: bool + # Use the cryptography library + use_cryptography = attrib(default=False) # type: bool + def __attrs_post_init__(self): """Initialize the Settings object""" # Setup hosts/ports/urls diff --git a/autopush/tests/test_integration.py b/autopush/tests/test_integration.py index a9ce9645..6fe5f58c 100644 --- a/autopush/tests/test_integration.py +++ b/autopush/tests/test_integration.py @@ -347,6 +347,7 @@ class IntegrationBase(unittest.TestCase): router_tablename=ROUTER_TABLE, storage_tablename=STORAGE_TABLE, message_tablename=MESSAGE_TABLE, + use_cryptography=True, ) _conn_defaults = dict( @@ -359,6 +360,7 @@ class IntegrationBase(unittest.TestCase): router_tablename=ROUTER_TABLE, storage_tablename=STORAGE_TABLE, message_tablename=MESSAGE_TABLE, + use_cryptography=True, ) def setUp(self): diff --git a/autopush/tests/test_web_validation.py b/autopush/tests/test_web_validation.py index f899d7d3..62748514 100644 --- a/autopush/tests/test_web_validation.py +++ b/autopush/tests/test_web_validation.py @@ -856,8 +856,9 @@ def test_valid_vapid_crypto_header(self): eq_(errors, {}) ok_("jwt" in result) - def test_valid_vapid_crypto_header_webpush(self): + def test_valid_vapid_crypto_header_webpush(self, use_crypto=False): schema = self._make_fut() + schema.context["settings"].use_cryptography = use_crypto header = {"typ": "JWT", "alg": "ES256"} payload = {"aud": "https://pusher_origin.example.com", @@ -887,6 +888,9 @@ def test_valid_vapid_crypto_header_webpush(self): eq_(errors, {}) ok_("jwt" in result) + def test_valid_vapid_crypto_header_webpush_crypto(self): + self.test_valid_vapid_crypto_header_webpush(use_crypto=True) + def test_valid_vapid_02_crypto_header_webpush(self): schema = self._make_fut() @@ -985,6 +989,8 @@ def test_bad_vapid_02_crypto_header(self): def test_invalid_vapid_draft2_crypto_header(self): schema = self._make_fut() + schema.context["settings"].use_cryptography = True + header = {"typ": "JWT", "alg": "ES256"} payload = {"aud": "https://pusher_origin.example.com", "exp": int(time.time()) + 86400, @@ -1018,6 +1024,8 @@ def test_invalid_vapid_draft2_crypto_header(self): @patch("autopush.web.webpush.extract_jwt") def test_invalid_vapid_crypto_header(self, mock_jwt): schema = self._make_fut() + schema.context["settings"].use_cryptography = True + mock_jwt.side_effect = ValueError("Unknown public key " "format specified") @@ -1154,6 +1162,7 @@ def test_invalid_encryption_header(self, mock_jwt): @patch("autopush.web.webpush.extract_jwt") def test_invalid_encryption_jwt(self, mock_jwt): schema = self._make_fut() + schema.context['settings'].use_cryptography = True # use a deeply superclassed error to make sure that it gets picked up. mock_jwt.side_effect = InvalidSignature("invalid signature") @@ -1225,6 +1234,7 @@ def test_invalid_crypto_key_header_content(self, mock_jwt): def test_expired_vapid_header(self): schema = self._make_fut() + schema.context["settings"].use_cryptography = True header = {"typ": "JWT", "alg": "ES256"} payload = {"aud": "https://pusher_origin.example.com", @@ -1291,6 +1301,7 @@ def test_missing_vapid_header(self): def test_bogus_vapid_header(self): schema = self._make_fut() + schema.context["settings"].use_cryptography = True header = {"typ": "JWT", "alg": "ES256"} payload = { diff --git a/autopush/tests/test_web_webpush.py b/autopush/tests/test_web_webpush.py index 6654f562..ffcc3019 100644 --- a/autopush/tests/test_web_webpush.py +++ b/autopush/tests/test_web_webpush.py @@ -37,6 +37,7 @@ def setUp(self): self.ap_settings = settings = AutopushSettings( hostname="localhost", statsd_host=None, + use_cryptography=True, ) self.fernet_mock = settings.fernet = Mock(spec=Fernet) diff --git a/autopush/utils.py b/autopush/utils.py index 4af9d687..5af4e68b 100644 --- a/autopush/utils.py +++ b/autopush/utils.py @@ -22,9 +22,11 @@ Tuple, ) from ua_parser import user_agent_parser +import ecdsa +from jose import jwt from autopush.exceptions import (InvalidTokenException, VapidAuthException) -from autopush.jwt import repad, VerifyJWT as jwt +from autopush.jwt import repad, VerifyJWT from autopush.types import ItemLike # noqa from autopush.web.base import AUTH_SCHEMES @@ -188,17 +190,31 @@ def decipher_public_key(key_data): raise ValueError("Unknown public key format specified") -def extract_jwt(token, crypto_key, is_trusted=False): +def extract_jwt(token, crypto_key, is_trusted=False, use_crypto=False): # type: (str, str, bool) -> Dict[str, str] """Extract the claims from the validated JWT. """ # first split and convert the jwt. if not token or not crypto_key: return {} if is_trusted: - return jwt.extract_assertion(token) - return jwt.validate_and_extract_assertion( - token, - decipher_public_key(crypto_key.encode('utf8'))) + return VerifyJWT.extract_assertion(token) + if use_crypto: + return VerifyJWT.validate_and_extract_assertion( + token, + decipher_public_key(crypto_key.encode('utf8'))) + else: + key = ecdsa.VerifyingKey.from_string( + base64.urlsafe_b64decode( + repad(crypto_key.encode('utf8')))[-64:], + curve=ecdsa.NIST256p + ) + return jwt.decode(token, + dict(keys=[key]), + options=dict( + verify_aud=False, + verify_sub=False, + verify_exp=False, + )) def parse_user_agent(agent_string): diff --git a/autopush/web/webpush.py b/autopush/web/webpush.py index 7753edbf..dd99031e 100644 --- a/autopush/web/webpush.py +++ b/autopush/web/webpush.py @@ -23,6 +23,7 @@ Dict, Optional ) +from jose import JOSEError, JWTError from autopush.crypto_key import CryptoKey from autopush.db import DatabaseManager # noqa @@ -356,6 +357,14 @@ def token_prep(self, d): return d def validate_auth(self, d): + crypto_exceptions = [KeyError, ValueError, TypeError, + VapidAuthException] + + if self.context['settings'].use_cryptography: + crypto_exceptions.append(InvalidSignature) + else: + crypto_exceptions.extend([JOSEError, JWTError, AssertionError]) + auth = d["headers"].get("authorization") needs_auth = d["token_info"]["api_ver"] == "v2" if not needs_auth and not auth: @@ -372,10 +381,10 @@ def validate_auth(self, d): jwt = extract_jwt( token, public_key, - is_trusted=self.context['settings'].enable_tls_auth + is_trusted=self.context['settings'].enable_tls_auth, + use_crypto=self.context['settings'].use_cryptography ) - except (KeyError, ValueError, InvalidSignature, TypeError, - VapidAuthException): + except tuple(crypto_exceptions): raise InvalidRequest("Invalid Authorization Header", status_code=401, errno=109, headers={"www-authenticate": PREF_SCHEME}) diff --git a/requirements.in b/requirements.in index 1e493580..a50ddf30 100644 --- a/requirements.in +++ b/requirements.in @@ -17,6 +17,7 @@ objgraph pyasn1 pyfcm pyopenssl +python-jose raven requests service-identity diff --git a/requirements.txt b/requirements.txt index ee180622..31aae795 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ cyclone==1.1 datadog==0.16.0 decorator==4.0.11 # via datadog docutils==0.13.1 # via botocore +ecdsa==0.13 # via python-jose enum34==1.1.6 # via h2 futures==3.1.1 # via s3transfer gcm-client==0.1.4 @@ -41,6 +42,7 @@ pycparser==2.17 # via cffi pyfcm==1.3.1 pyopenssl==17.1.0 python-dateutil==2.6.1 # via botocore +python-jose==1.3.2 raven==6.1.0 requests-toolbelt==0.8.0 # via pyfcm requests==2.18.1