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

Commit

Permalink
feature: allow channels to register with public key
Browse files Browse the repository at this point in the history
Channels can include a public key when registering a new subscription
channel. This public key should match the public key used to send
subscription updates later.

NOTE: this patch changes the format of the endpoint URLs, & the content
of the endpoint URL token. This change also requires that ChannelIDs be
normalized to dashed format, (e.g. a lower case, dash delimited string
"deadbeef-0000-0000-deca-fbad11112222") This is the default mechanism
used by Firefox for UUID generation. It is STRONGLY urged that clients
normalize UUIDs used for ChannelIDs and User Agent IDs. While this
should not break existing clients, additional testing may be required.

Closes: #326
  • Loading branch information
jrconlin committed Feb 29, 2016
1 parent 108880d commit cf00be8
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 29 deletions.
54 changes: 30 additions & 24 deletions autopush/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def _invalid_auth(self, fail):
log.msg("Invalid bearer token: " + message, **self._client_info)
raise VapidAuthException("Invalid bearer token: " + message)

def _process_auth(self, result):
def _process_auth(self, result, keys):
"""Process the optional VAPID auth token.
VAPID requires two headers to be present;
Expand All @@ -317,28 +317,25 @@ def _process_auth(self, result):
if not authorization:
return result

header_info = parse_header(self.request.headers.get('crypto-key'))
if not header_info:
if keys is None:
raise VapidAuthException("Missing Crypto-Key")
values = header_info[-1]
if isinstance(values, dict):
crypto_key = values.get('p256ecdsa')
try:
(auth_type, token) = authorization.split(' ', 1)
except ValueError:
raise VapidAuthException("Invalid Authorization header")
# if it's a bearer token containing what may be a JWT
if auth_type.lower() == AUTH_SCHEME and '.' in token:
d = deferToThread(extract_jwt, token, crypto_key)
d.addCallback(self._store_auth, crypto_key, token, result)
d.addErrback(self._invalid_auth)
return d
# otherwise, it's not, so ignore the VAPID data.
return result
else:

crypto_key = keys.get('p256ecdsa')
if not crypto_key:
raise VapidAuthException("Invalid bearer token: "
"improperly specified crypto-key")

try:
(auth_type, token) = authorization.split(' ', 1)
except ValueError:
raise VapidAuthException("Invalid Authorization header")
# if it's a bearer token containing what may be a JWT
if auth_type.lower() == AUTH_SCHEME and '.' in token:
d = deferToThread(extract_jwt, token, crypto_key)
d.addCallback(self._store_auth, crypto_key, token, result)
d.addErrback(self._invalid_auth)
return d
# otherwise, it's not, so ignore the VAPID data.
return result

class MessageHandler(AutoendpointHandler):
cors_methods = "DELETE"
Expand Down Expand Up @@ -405,10 +402,19 @@ def put(self, token):
"""
self.start_time = time.time()
fernet = self.ap_settings.fernet

d = deferToThread(fernet.decrypt, token.encode('utf8'))
d.addCallback(self._process_auth)
public_key = None
keys = {}
crypto_key = self.request.headers.get('crypto-key')
if crypto_key:
header_info = parse_header(crypto_key)
keys = header_info[-1]
if isinstance(keys, dict):
public_key = keys.get('p256ecdsa')

d = deferToThread(self.ap_settings.parse_endpoint,
token,
public_key)
d.addCallback(self._process_auth, keys)
d.addCallback(self._token_valid)
d.addErrback(self._auth_err)
d.addErrback(self._token_err)
Expand Down
70 changes: 66 additions & 4 deletions autopush/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import datetime
import socket

from hashlib import sha256

from cryptography.fernet import Fernet, MultiFernet
from twisted.internet import reactor
from twisted.internet.defer import (
Expand Down Expand Up @@ -248,7 +250,67 @@ def update(self, **kwargs):
else:
setattr(self, key, val)

def make_endpoint(self, uaid, chid):
""" Create an endpoint from the identifiers"""
return self.endpoint_url + '/push/' + \
self.fernet.encrypt((uaid + ':' + chid).encode('utf8'))
def make_endpoint(self, uaid, chid, key=None):
"""Create an v1 or v2 endpoint from the indentifiers.
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 provided Public Key
:returns: Push endpoint
"""
root = self.endpoint_url + '/push/'
base = uaid.decode("hex") + chid.decode("hex")

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

return root + 'v2/' + self.fernet.encrypt(base + sha256(key).digest())

def parse_endpoint(self, token, public_key=None):
"""Parse an endpoint into component elements of UAID, CHID and optional
key hash if v2
:param endpoint: this is either a path string or a full URL that will
be parsed into a path string.
:param public_key: the public key (from Encryption-Key: p256ecdsa=)
:raises ValueError: In the case of a malformed endpoint.
:returns: a tuple containing the (UAID, CHID, Public Key hash)
"""
import pdb;pdb.set_trace()
path = token.split('/')
if path[0] is '' and len(path) > 1:
path = path[:1]
if len(path) == 1:
ver = 'v0'
token = path[0].encode('utf8')
else:
ver = path[0]
token = path[1]

d = deferToThread(self.fernet.decrypt, token)
d.addCallback(self._handle_tokens, ver, public_key)
return d

def _handle_tokens(self, token, version='v0', public_key=None):
if version is 'v0':
if ':' not in token:
raise ValueError("Corrupted push token")
return token.split(':')
import pdb;pdb.set_trace()
if version is 'v1' and len(token) != 32:
raise ValueError("Corrupted push token")
if version is 'v2':
if len(token) != 64:
raise ValueError("Corrupted push token")
if sha256(public_key).digest is not token[32:]:
raise ValueError("Key Mismatch")
return (token[:16].encode('hex'), token[16:32].encode('hex'))
2 changes: 1 addition & 1 deletion autopush/tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def setUp(self, t):
# so slow, that many of these tests will time out leading
# to false failure rates and integration tests generally
# failing.
self.timeout = 1
#self.timeout = 1

twisted.internet.base.DelayedCall.debug = True

Expand Down

0 comments on commit cf00be8

Please sign in to comment.