Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
feat: Validate Encryption/C-Key headers in preflight
Browse files Browse the repository at this point in the history
Check the encryption headers to make sure they're roughly valid before
passing them on to the client.

Closes #456
  • Loading branch information
jrconlin committed Aug 12, 2016
1 parent 0409368 commit 0c27efc
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 46 deletions.
11 changes: 11 additions & 0 deletions autopush/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,17 @@ def _uaid_lookup_results(self, result):
400, 110, message="Invalid crypto headers")
return
self._client_info["message_size"] = len(data) if data else 0
if ("crypto-key" in self.request.headers and
"dh=" not in self.request.headers['crypto-key']):
self._write_response(
401, 110, message="Crypto-Key header missing public-key "
"'dh' value")
return
if ("encryption" in self.request.headers and
"salt=" not in self.request.headers['encryption']):
self._write_response(
401, 110, message="Encryption header missing 'salt' value")
return

if "ttl" not in self.request.headers:
ttl = None
Expand Down
104 changes: 71 additions & 33 deletions autopush/tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def test_uaid_lookup_results(self):
frouter.route_notification = Mock()
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.endpoint.ap_settings.routers["test"] = frouter
self.endpoint._uaid_lookup_results(fresult)
Expand All @@ -237,7 +237,7 @@ def test_uaid_lookup_results_bad_ttl(self):
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["ttl"] = "woops"
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.endpoint.ap_settings.routers["test"] = frouter
self.endpoint._uaid_lookup_results(fresult)
Expand All @@ -256,7 +256,7 @@ def test_webpush_ttl_too_large(self):
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["ttl"] = str(MAX_TTL + 100)
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.endpoint.ap_settings.routers["test"] = frouter
self.endpoint._uaid_lookup_results(fresult)
Expand Down Expand Up @@ -289,7 +289,7 @@ def test_webpush_missing_ttl_user_offline(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
del(self.request_mock.headers["ttl"])
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down Expand Up @@ -318,13 +318,10 @@ def handle_finish(result):
def test_webpush_malformed_encryption(self):

def handle_finish(value):
err_msg = ("You're using outdated encryption; Please update "
"to the format described in "
"https://developers.google.com/web/updates/2016/"
"03/web-push-encryption")
self._check_error(400, 110, message=err_msg)
self._check_error(400, 110)
self.request_mock.headers["content-encoding"] = "aesgcm128"
self.request_mock.headers["crypto-key"] = "content"
self.request_mock.headers["crypto-key"] = "dh=content"
self.request_mock.headers["encryption"] = "salt=salt"

self.finish_deferred.addCallback(handle_finish)
self.endpoint.put(None, dummy_uaid)
Expand Down Expand Up @@ -391,7 +388,7 @@ def test_webpush_payload_encoding(self):
frouter = self.settings.routers["webpush"]
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["encryption"] = "keyid=p256;dh=stuff=="
self.request_mock.headers["encryption"] = "keyid=p256;salt=stuff=="
self.request_mock.headers["crypto-key"] = (
"keyid=spad=;dh=AQ==,p256ecdsa=Ag=;foo=\"bar==\""
)
Expand All @@ -404,7 +401,7 @@ def handle_finish(value):
eq_(len(calls), 1)
(_, (notification, _), _) = calls[0]
eq_(notification.headers.get('encryption'),
'keyid=p256;dh=stuff')
'keyid=p256;salt=stuff')
eq_(notification.headers.get('crypto-key'),
'keyid=spad;dh=AQ,p256ecdsa=Ag;foo=bar')
eq_(notification.channel_id, dummy_chid)
Expand All @@ -416,6 +413,46 @@ def handle_finish(value):
self.finish_deferred.addCallback(handle_finish)
return self.finish_deferred

def test_webpush_bad_ckey(self):
fresult = dict(router_type="webpush")
frouter = self.settings.routers["webpush"]
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["crypto-key"] = "invalid"
self.request_mock.headers["content-encoding"] = "aesgcm"
self.request_mock.body = b"\xc3\x28\xa0\xa1"
self.endpoint._uaid_lookup_results(fresult)

def handle_finish(value):
self.endpoint.set_status.assert_called_with(401, None)
self._check_error(code=401, errno=110,
message="Crypto-Key header missing "
"public-key 'dh' value")

self.finish_deferred.addCallback(handle_finish)
return self.finish_deferred

def test_webpush_bad_salt(self):
fresult = dict(router_type="webpush")
frouter = self.settings.routers["webpush"]
frouter.route_notification.return_value = RouterResponse()
self.endpoint.chid = dummy_chid
self.request_mock.headers["encryption"] = "invalid"
self.request_mock.headers["crypto-key"] = "dh=valid"
self.request_mock.headers["content-encoding"] = "aesgcm"
self.request_mock.body = b"\xc3\x28\xa0\xa1"
self.endpoint._uaid_lookup_results(fresult)

def handle_finish(value):
self.endpoint.set_status.assert_called_with(401, None)
self._check_error(code=401, errno=110,
message="Encryption header missing "
"'salt' value")

self.finish_deferred.addCallback(handle_finish)
return self.finish_deferred

def test_init_info(self):
d = self.endpoint._init_info()
eq_(d["request_id"], dummy_request_id)
Expand Down Expand Up @@ -586,7 +623,7 @@ def handle_finish(result):
return self.finish_deferred

def test_put_router_with_headers(self):
self.request_mock.headers["encryption"] = "ignored"
self.request_mock.headers["encryption"] = "salt=ignored"
self.request_mock.headers["content-encoding"] = 'text'
self.request_mock.headers["encryption-key"] = "encKey"
self.request_mock.body = b' '
Expand Down Expand Up @@ -653,10 +690,10 @@ def handle_finish(result):
return self.finish_deferred

def test_put_bogus_headers(self):
self.request_mock.headers["encryption"] = "ignored"
self.request_mock.headers["encryption"] = "salt=ignored"
self.request_mock.headers["content-encoding"] = 'text'
self.request_mock.headers["encryption-key"] = "encKey"
self.request_mock.headers["crypto-key"] = "fake=crypKey"
self.request_mock.headers["crypto-key"] = "dh=crypKey"
self.request_mock.body = b' '
self.fernet_mock.decrypt.return_value = dummy_token
self.router_mock.get_uaid.return_value = dict(
Expand All @@ -677,7 +714,7 @@ def handle_finish(result):
return self.finish_deferred

def test_put_invalid_vapid_crypto_header(self):
self.request_mock.headers["encryption"] = "ignored"
self.request_mock.headers["encryption"] = "salt=ignored"
self.request_mock.headers["content-encoding"] = 'text'
self.request_mock.headers["authorization"] = "some auth"
self.request_mock.headers["crypto-key"] = "crypKey"
Expand All @@ -701,7 +738,7 @@ def handle_finish(result):
return self.finish_deferred

def test_put_invalid_vapid_crypto_key(self):
self.request_mock.headers["encryption"] = "ignored"
self.request_mock.headers["encryption"] = "salt=ignored"
self.request_mock.headers["content-encoding"] = 'text'
self.request_mock.headers["authorization"] = "invalid"
self.request_mock.headers["crypto-key"] = "crypt=crap"
Expand All @@ -725,7 +762,7 @@ def handle_finish(result):
return self.finish_deferred

def test_put_invalid_vapid_auth_header(self):
self.request_mock.headers["encryption"] = "ignored"
self.request_mock.headers["encryption"] = "salt=ignored"
self.request_mock.headers["content-encoding"] = 'text'
self.request_mock.headers["authorization"] = "invalid"
self.request_mock.headers["crypto-key"] = "p256ecdsa=crap"
Expand All @@ -751,7 +788,7 @@ def handle_finish(result):
def test_post_webpush_with_headers_in_response(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down Expand Up @@ -782,7 +819,7 @@ def _gen_jwt(self, header, payload):
def test_post_webpush_with_vapid_auth(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand All @@ -800,7 +837,7 @@ def test_post_webpush_with_vapid_auth(self):
eq_(res, payload)
"""
self.request_mock.headers["crypto-key"] = \
"keyid=\"a1\"; key=\"foo\";p256ecdsa=\"%s\"" % crypto_key
"keyid=\"a1\";dh=\"foo\";p256ecdsa=\"%s\"" % crypto_key
self.request_mock.headers["authorization"] = auth
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down Expand Up @@ -846,7 +883,7 @@ def test_decipher_public_key(self):
def test_post_webpush_with_other_than_vapid_auth(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand All @@ -856,7 +893,8 @@ def test_post_webpush_with_other_than_vapid_auth(self):

(token, crypto_key) = self._gen_jwt(header, payload)
auth = "WebPush other_token"
self.request_mock.headers["crypto-key"] = "p256ecdsa=%s" % crypto_key
self.request_mock.headers["crypto-key"] = (
"dh=stuff;p256ecdsa=%s" % crypto_key)
self.request_mock.headers["authorization"] = auth
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand All @@ -878,7 +916,7 @@ def handle_finish(result):
def test_post_webpush_with_bad_vapid_auth(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand Down Expand Up @@ -910,7 +948,7 @@ def handle_finish(result):
def test_post_webpush_no_sig(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand Down Expand Up @@ -957,7 +995,7 @@ def test_util_extract_jwt(self):
def test_post_webpush_bad_sig(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand Down Expand Up @@ -991,7 +1029,7 @@ def handle_finish(result):
def test_post_webpush_bad_exp(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"

header = {"typ": "JWT", "alg": "ES256"}
Expand Down Expand Up @@ -1023,9 +1061,9 @@ def handle_finish(result):
def test_post_webpush_with_auth(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.request_mock.headers["crypto-key"] = ""
self.request_mock.headers["crypto-key"] = "dh=foo"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
router_data=dict(),
Expand All @@ -1048,7 +1086,7 @@ def handle_finish(result):
def test_post_webpush_with_logged_delivered(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand All @@ -1075,7 +1113,7 @@ def handle_finish(result):
def test_post_webpush_with_logged_stored(self):
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down Expand Up @@ -1104,7 +1142,7 @@ def test_post_db_error_with_success(self):
from autopush.router.interface import RouterException
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down Expand Up @@ -1133,7 +1171,7 @@ def test_post_db_error_in_routing(self):
from autopush.router.interface import RouterException
self.fernet_mock.decrypt.return_value = dummy_token
self.endpoint.set_header = Mock()
self.request_mock.headers["encryption"] = "stuff"
self.request_mock.headers["encryption"] = "salt=stuff"
self.request_mock.headers["content-encoding"] = "aes128"
self.router_mock.get_uaid.return_value = dict(
router_type="webpush",
Expand Down
2 changes: 1 addition & 1 deletion autopush/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def send_notification(self, channel=None, version=None, data=None,
"Content-Type": "application/octet-stream",
"Content-Encoding": "aesgcm-128",
"Encryption": self._crypto_key,
"Crypto-Key": 'keyid="a1"; key="JcqK-OLkJZlJ3sJJWstJCA"',
"Crypto-Key": 'keyid="a1"; dh="JcqK-OLkJZlJ3sJJWstJCA"',
})
if vapid:
headers.update({
Expand Down
Loading

0 comments on commit 0c27efc

Please sign in to comment.