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
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
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
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
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
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
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']
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)
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)
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)
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)
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
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
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