Пример #1
0
    def test_invalid_encryption_jwt(self, mock_jwt):
        schema = self._make_fut()
        schema.context['conf'].use_cryptography = True
        # use a deeply superclassed error to make sure that it gets picked up.
        mock_jwt.side_effect = InvalidSignature("invalid signature")

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://push.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        self.fernet_mock.decrypt.return_value = ('a'*32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        auth = "Bearer %s" % token
        ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                        "crypto-key": ckey
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)

        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #2
0
    def test_invalid_bad_exp_vapid_crypto_header(self):
        schema = self._make_fut()
        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": "bleh",
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        auth = "WebPush %s" % token
        self.fernet_mock.decrypt.return_value = ('a'*32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                        "crypto-key": ckey
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)

        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #3
0
    def test_invalid_encryption_header(self, mock_jwt):
        schema = self._make_fut()
        mock_jwt.side_effect = ValueError("Unknown public key "
                                          "format specified")

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        self.fernet_mock.decrypt.return_value = ('a'*32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        auth = "Bearer %s" % token
        ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                        "crypto-key": ckey
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)

        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #4
0
    def test_bad_vapid_02_crypto_header(self):
        schema = self._make_fut()

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        # Missing one of the two required parameters, t & k
        auth = "vapid t={token},n={key}".format(token=token, key=crypto_key)
        self.fernet_mock.decrypt.return_value = ('a' * 32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)
        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #5
0
    def test_invalid_vapid_draft2_crypto_header(self):
        schema = self._make_fut()
        schema.context["conf"].use_cryptography = True

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        self.fernet_mock.decrypt.return_value = ('a'*32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        # Corrupt the token so it fails. (Mock doesn't always catch)
        auth = "vapid t={token},k={key}".format(token=token + "foo",
                                                key=crypto_key)
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)

        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #6
0
    def test_valid_vapid_crypto_header_webpush(self, use_crypto=False):
        schema = self._make_fut()
        schema.context["conf"].use_cryptography = use_crypto

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        auth = "WebPush %s" % token
        self.fernet_mock.decrypt.return_value = ('a'*32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                        "crypto-key": ckey
                                    })

        result, errors = schema.load(info)
        assert errors == {}
        assert "jwt" in result
Пример #7
0
    def test_valid_vapid_02_crypto_header_webpush_alt(self):
        schema = self._make_fut()

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": int(time.time()) + 86400,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        # Switch the params and add an extra, ignored parameter
        auth = "vapid k={key},  t={token},   foo=bar".format(token=token,
                                                             key=crypto_key)
        self.fernet_mock.decrypt.return_value = ('a' * 32) + \
            sha256(utils.base64url_decode(crypto_key)).digest()
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "authorization": auth,
                                    })

        result, errors = schema.load(info)
        assert errors == {}
        assert "jwt" in result
        assert payload == result['jwt']['jwt_data']
Пример #8
0
    def test_bogus_vapid_header(self):
        schema = self._make_fut()

        header = {"typ": "JWT", "alg": "ES256"}
        payload = {
            "aud": "https://pusher_origin.example.com",
            "exp": 20,
            "sub": "mailto:[email protected]"
        }

        token, crypto_key = self._gen_jwt(header, payload)
        self.fernet_mock.decrypt.return_value = ('a' * 32) + sha256(
            utils.base64url_decode(crypto_key)).digest()
        ckey = 'keyid="a1"; dh="foo";p256ecdsa="%s"' % crypto_key
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding": "aesgcm",
                                        "encryption": "salt=stuff",
                                        "crypto-key": ckey,
                                        "authorization": "bogus crap"
                                    })

        with assert_raises(InvalidRequest) as cm:
            schema.load(info)

        eq_(cm.exception.status_code, 401)
        eq_(cm.exception.errno, 109)
Пример #9
0
    def parse_endpoint(self,
                       metrics,
                       token,
                       version="v1",
                       ckey_header=None,
                       auth_header=None):
        """Parse an endpoint into component elements of UAID, CHID and optional
        key hash if v2

        :param token: The obscured subscription data.
        :param version: This is the API version of the token.
        :param ckey_header: the Crypto-Key header bearing the public key
            (from Crypto-Key: p256ecdsa=)
        :param auth_header: The Authorization header bearing the VAPID info

        :raises ValueError: In the case of a malformed endpoint.

        :returns: a dict containing (uaid=UAID, chid=CHID, public_key=KEY)

        """
        token = self.fernet.decrypt(repad(token).encode('utf8'))
        public_key = None
        if ckey_header:
            try:
                crypto_key = CryptoKey(ckey_header)
            except CryptoKeyException:
                raise InvalidTokenException("Invalid key data")
            public_key = crypto_key.get_label('p256ecdsa')
        if auth_header:
            vapid_auth = parse_auth_header(auth_header)
            if not vapid_auth:
                raise VapidAuthException("Invalid Auth token")
            metrics.increment("updates.notification.auth.{}".format(
                vapid_auth['scheme']))
            # pull the public key from the VAPID auth header if needed
            try:
                if vapid_auth['version'] != 1:
                    public_key = vapid_auth['k']
            except KeyError:
                raise VapidAuthException("Missing Public Key")
        if version == 'v1' and len(token) != 32:
            raise InvalidTokenException("Corrupted push token")
        if version == 'v2':
            if not auth_header:
                raise VapidAuthException("Missing Authorization Header")
            if len(token) != 64:
                raise InvalidTokenException("Corrupted push token")
            if not public_key:
                raise VapidAuthException("Invalid key data")
            try:
                decoded_key = base64url_decode(public_key)
            except TypeError:
                raise VapidAuthException("Invalid key data")
            if not constant_time.bytes_eq(
                    sha256(decoded_key).digest(), token[32:]):
                raise VapidAuthException("Key mismatch")
        return dict(uaid=token[:16].encode('hex'),
                    chid=token[16:32].encode('hex'),
                    version=version,
                    public_key=public_key)
Пример #10
0
    def parse_endpoint(self, token, version="v0", ckey_header=None):
        """Parse an endpoint into component elements of UAID, CHID and optional
        key hash if v2

        :param token: The obscured subscription data.
        :param version: This is the API version of the token.
        :param ckey_header: the Crypto-Key header bearing the public key
        (from Crypto-Key: p256ecdsa=)

        :raises ValueError: In the case of a malformed endpoint.

        :returns: a dict containing (uaid=UAID, chid=CHID, public_key=KEY)

        """

        token = self.fernet.decrypt(token.encode('utf8'))
        public_key = None
        if ckey_header:
            try:
                crypto_key = CryptoKey(ckey_header)
            except CryptoKeyException:
                raise InvalidTokenException("Invalid key data")
            label = crypto_key.get_label('p256ecdsa')
            try:
                public_key = base64url_decode(label)
            except:
                # Ignore missing and malformed app server keys.
                pass

        if version == 'v0':
            if not VALID_V0_TOKEN.match(token):
                raise InvalidTokenException("Corrupted push token")
            items = token.split(':')
            return dict(uaid=items[0], chid=items[1], public_key=public_key)
        if version == 'v1' and len(token) != 32:
            raise InvalidTokenException("Corrupted push token")
        if version == 'v2':
            if len(token) != 64:
                raise InvalidTokenException("Corrupted push token")
            if not public_key:
                raise InvalidTokenException("Invalid key data")
            if not constant_time.bytes_eq(sha256(public_key).digest(),
                                          token[32:]):
                raise InvalidTokenException("Key mismatch")
        return dict(uaid=token[:16].encode('hex'),
                    chid=token[16:32].encode('hex'),
                    public_key=public_key)
Пример #11
0
    def parse_endpoint(self, token, version="v0", ckey_header=None):
        """Parse an endpoint into component elements of UAID, CHID and optional
        key hash if v2

        :param token: The obscured subscription data.
        :param version: This is the API version of the token.
        :param ckey_header: the Crypto-Key header bearing the public key
        (from Crypto-Key: p256ecdsa=)

        :raises ValueError: In the case of a malformed endpoint.

        :returns: a dict containing (uaid=UAID, chid=CHID, public_key=KEY)

        """

        token = self.fernet.decrypt(token.encode('utf8'))
        public_key = None
        if ckey_header:
            try:
                crypto_key = CryptoKey(ckey_header)
            except CryptoKeyException:
                raise InvalidTokenException("Invalid key data")
            label = crypto_key.get_label('p256ecdsa')
            try:
                public_key = base64url_decode(label)
            except:
                # Ignore missing and malformed app server keys.
                pass

        if version == 'v0':
            if not VALID_V0_TOKEN.match(token):
                raise InvalidTokenException("Corrupted push token")
            items = token.split(':')
            return dict(uaid=items[0], chid=items[1], public_key=public_key)
        if version == 'v1' and len(token) != 32:
            raise InvalidTokenException("Corrupted push token")
        if version == 'v2':
            if len(token) != 64:
                raise InvalidTokenException("Corrupted push token")
            if not public_key:
                raise InvalidTokenException("Invalid key data")
            if not constant_time.bytes_eq(
                    sha256(public_key).digest(), token[32:]):
                raise InvalidTokenException("Key mismatch")
        return dict(uaid=token[:16].encode('hex'),
                    chid=token[16:32].encode('hex'),
                    public_key=public_key)
Пример #12
0
    def make_endpoint(self, uaid, chid, key=None):
        """Create an v1 or v2 WebPush endpoint from the identifiers.

        Both endpoints use bytes instead of hex to reduce ID length.
        v1 is the uaid + chid
        v2 is the uaid + chid + sha256(key).bytes

        :param uaid: User Agent Identifier
        :param chid: Channel or Subscription ID
        :param key: Optional Base64 URL-encoded application server key
        :returns: Push endpoint

        """
        root = self.endpoint_url + '/wpush/'
        base = (uaid.replace('-', '').decode("hex") +
                chid.replace('-', '').decode("hex"))

        if key is None:
            return root + 'v1/' + self.fernet.encrypt(base).strip('=')

        raw_key = base64url_decode(key.encode('utf8'))
        ep = self.fernet.encrypt(base + sha256(raw_key).digest()).strip('=')
        return root + 'v2/' + ep
Пример #13
0
    def test_null_vapid_header(self):
        schema = self._make_fut()
        schema.context["conf"].use_cryptography = True

        def b64s(content):
            return base64.urlsafe_b64encode(content).strip(b'=')

        payload = b'.'.join([b64s("null"), b64s("null")])

        # force sign the header, since jws will "fix" the invalid one.
        sk256p = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
        vk = sk256p.get_verifying_key()
        key = jwk.construct(sk256p, "ES256")
        signature = b64s(key.sign(payload))
        token = b'.'.join([payload, signature])
        crypto_key = b64s(vk.to_string())

        self.fernet_mock.decrypt.return_value = ('a' * 32) + sha256(
            utils.base64url_decode(crypto_key)).digest()
        info = self._make_test_data(body="asdfasdfasdfasdf",
                                    path_kwargs=dict(
                                        api_ver="v2",
                                        token="asdfasdf",
                                    ),
                                    headers={
                                        "content-encoding":
                                        "aes128gcm",
                                        "authorization":
                                        "vapid k={},t={}".format(
                                            crypto_key, token)
                                    })

        with pytest.raises(InvalidRequest) as cm:
            schema.load(info)

        assert cm.value.status_code == 401
        assert cm.value.errno == 109
Пример #14
0
    def make_endpoint(self, uaid, chid, key=None):
        """Create an v1 or v2 WebPush endpoint from the identifiers.

        Both endpoints use bytes instead of hex to reduce ID length.
        v0 is uaid.hex + ':' + chid.hex and is deprecated.
        v1 is the uaid + chid
        v2 is the uaid + chid + sha256(key).bytes

        :param uaid: User Agent Identifier
        :param chid: Channel or Subscription ID
        :param key: Optional Base64 URL-encoded application server key
        :returns: Push endpoint

        """
        root = self.endpoint_url + '/push/'
        base = (uaid.replace('-', '').decode("hex") +
                chid.replace('-', '').decode("hex"))

        if key is None:
            return root + 'v1/' + self.fernet.encrypt(base).strip('=')

        raw_key = base64url_decode(key.encode('utf8'))
        ep = self.fernet.encrypt(base + sha256(raw_key).digest()).strip('=')
        return root + 'v2/' + ep