Skip to content

Commit

Permalink
Feat: 3.4 support and JSON dump
Browse files Browse the repository at this point in the history
There were several raw print statements that needed fixing. Also:
`--json` will now dump the headers as a JSON blob. This should make
importing the data into other functions a bit easier.

closes #29, #30
  • Loading branch information
jrconlin committed Mar 22, 2017
1 parent 2b3d084 commit b747d2d
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 61 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
language: python
python:
- "2.7"
- "3.5"
install:
- cd python
- pip install -r requirements.txt
- pip install -r test-requirements.txt
script:
- nosetests
- flake8 pywebpush
- flake8 py_vapid
after_success:
- coverage report --omit py_vapid/main.py
2 changes: 1 addition & 1 deletion python/MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ include *.md
include *.txt
include setup.*
include LICENSE
recursive-include py_vapid
recursive-include py_vapid *.py
60 changes: 54 additions & 6 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,50 @@ This minimal library contains the minimal set of functions you need to
generate a VAPID key set and get the headers you'll need to sign a
WebPush subscription update.

This can either be installed as a library or used as a stand along
app.
VAPID is a voluntary standard for WebPush subscription providers
(sites that send WebPush updates to remote customers) to self-identify
to Push Servers (the servers that convey the push notifications).

The VAPID "claims" are a set of JSON keys and values. There are two
required fields, one semi-optional and several optional additional
fields.

At a minimum a VAPID claim set should look like:
```
{"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServerURL","exp":"ExpirationTimestamp"}
```
A few notes:

***sub*** is the email address you wish to have on record for this
request, prefixed with "`mailto:`". If things go wrong, this is the
email that will be used to contact you (for instance). This can be a
general delivery address like "`mailto:push_operations@example.com`" or a
specific address like "`mailto:bob@example.com`".

***aud*** is the audience for the VAPID. This it the host path you use to
send subscription endpoints and generally coincides with the
`endpoint` specified in the Subscription Info block.

As example, if a WebPush subscription info contains:
`{"endpoint": "https://push.example.com:8012/v1/push/...", ...}`

then the `aud` would be "`https://push.example.com:8012/`"

While some Push Services consider this an optional field, others may
be stricter.

***exp*** This is the UTC timestamp for when this VAPID request will
expire. The maximum period is 24 hours. Setting a shorter period can
prevent "replay" attacks. Setting a longer period allows you to reuse
headers for multiple sends (e.g. if you're sending hundreds of updates
within an hour or so.) If no `exp` is included, one that will expire
in 24 hours will be auto-generated for you.

Claims should be stored in a JSON compatible file. In the examples
below, we've stored the claims into a file named `claims.json`.

py_vapid can either be installed as a library or used as a stand along
app, `bin/vapid`.

## App Installation

Expand All @@ -15,18 +57,24 @@ Then run
```
bin/pip install -r requirements.txt
bin/python setup.py`install
bin/python setup.py install
```
## App Usage

Run by itself, `bin/vapid` will check and optionally create the
public_key.pem and private_key.pem files.

`bin/vapid --sign _claims.json_` will generate a set of HTTP headers
`bin/vapid -gen` can be used to generate a new set of public and
private key PEM files. These will overwrite the contents of
`private_key.pem` and `public_key.pem`.

`bin/vapid --sign claims.json` will generate a set of HTTP headers
from a JSON formatted claims file. A sample `claims.json` is included
with this distribution.

`bin/vapid --validate _token_` will generate a token response for the
Mozilla WebPush dashboard.
`bin/vapid --sign claims.json --json` will output the headers in
JSON format, which may be useful for other programs.

See `bin/vapid -h` for all options and commands.


85 changes: 85 additions & 0 deletions python/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Easy VAPID generation
=====================

This minimal library contains the minimal set of functions you need to
generate a VAPID key set and get the headers you'll need to sign a
WebPush subscription update.

VAPID is a voluntary standard for WebPush subscription providers (sites
that send WebPush updates to remote customers) to self-identify to Push
Servers (the servers that convey the push notifications).

The VAPID "claims" are a set of JSON keys and values. There are two
required fields, one semi-optional and several optional additional
fields.

At a minimum a VAPID claim set should look like:

::

{"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServerURL","exp":"ExpirationTimestamp"}

A few notes:

***sub*** is the email address you wish to have on record for this
request, prefixed with "``mailto:``". If things go wrong, this is the
email that will be used to contact you (for instance). This can be a
general delivery address like "``mailto:push_operations@example.com``"
or a specific address like "``mailto:bob@example.com``".

***aud*** is the audience for the VAPID. This it the host path you use
to send subscription endpoints and generally coincides with the
``endpoint`` specified in the Subscription Info block.

As example, if a WebPush subscription info contains:
``{"endpoint": "https://push.example.com:8012/v1/push/...", ...}``

then the ``aud`` would be "``https://push.example.com:8012/``"

While some Push Services consider this an optional field, others may be
stricter.

***exp*** This is the UTC timestamp for when this VAPID request will
expire. The maximum period is 24 hours. Setting a shorter period can
prevent "replay" attacks. Setting a longer period allows you to reuse
headers for multiple sends (e.g. if you're sending hundreds of updates
within an hour or so.) If no ``exp`` is included, one that will expire
in 24 hours will be auto-generated for you.

Claims should be stored in a JSON compatible file. In the examples
below, we've stored the claims into a file named ``claims.json``.

py\_vapid can either be installed as a library or used as a stand along
app, ``bin/vapid``.

App Installation
----------------

You'll need ``python virtualenv`` Run that in the current directory.

Then run

::

bin/pip install -r requirements.txt

bin/python setup.py install

App Usage
---------

Run by itself, ``bin/vapid`` will check and optionally create the
public\_key.pem and private\_key.pem files.

``bin/vapid -gen`` can be used to generate a new set of public and
private key PEM files. These will overwrite the contents of
``private_key.pem`` and ``public_key.pem``.

``bin/vapid --sign claims.json`` will generate a set of HTTP headers
from a JSON formatted claims file. A sample ``claims.json`` is included
with this distribution.

``bin/vapid --sign claims.json --json`` will output the headers in JSON
format, which may be useful for other programs.

See ``bin/vapid -h`` for all options and commands.
12 changes: 8 additions & 4 deletions python/py_vapid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ def validate(self, validation_token):
:rtype: str
"""
sig = self.private_key.sign(validation_token,
sig = self.private_key.sign(
validation_token,
hashfunc=self._hasher)
verification_token = base64.urlsafe_b64encode(sig)
return verification_token
Expand All @@ -149,7 +150,7 @@ def verify_token(self, validation_token, verification_token):

def _base_sign(self, claims):
if not claims.get('exp'):
claims['exp'] = int(time.time()) + 86400
claims['exp'] = str(int(time.time()) + 86400)
if not claims.get('sub'):
raise VapidException(
"Missing 'sub' from claims. "
Expand All @@ -176,9 +177,12 @@ def sign(self, claims, crypto_key=None):
claims = self._base_sign(claims)
sig = jws.sign(claims, self.private_key, algorithm="ES256")
pkey = 'p256ecdsa='
pkey += self.encode(self.public_key.to_string())
pubkey = self.public_key.to_string()
if len(pubkey) == 64:
pubkey = b'\04' + pubkey
pkey += self.encode(pubkey)
if crypto_key:
crypto_key = crypto_key + ',' + pkey
crypto_key = crypto_key + ';' + pkey
else:
crypto_key = pkey

Expand Down
82 changes: 46 additions & 36 deletions python/py_vapid/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,54 @@
def main():
parser = argparse.ArgumentParser(description="VAPID tool")
parser.add_argument('--sign', '-s', help='claims file to sign')
parser.add_argument('--gen', '-g', help='generate new key pairs',
default=False, action="store_true")
parser.add_argument('--validate', '-v', help='dashboard token to validate')
parser.add_argument('--version2', '-2', help="use VAPID spec Draft-02",
default=False, action="store_true")
parser.add_argument('--version1', '-1', help="use VAPID spec Draft-01",
default=True, action="store_true")
parser.add_argument('--json', help="dump as json",
default=False, action="store_true")
args = parser.parse_args()
Vapid = Vapid01
if args.version2:
Vapid = Vapid02
if not os.path.exists('private_key.pem'):
print "No private_key.pem file found."
answer = None
while answer not in ['y', 'n']:
answer = raw_input("Do you want me to create one for you? (Y/n)")
if not answer:
answer = 'y'
answer = answer.lower()[0]
if answer == 'n':
print "Sorry, can't do much for you then."
exit
if answer == 'y':
break
if args.gen or not os.path.exists('private_key.pem'):
if not args.gen:
print("No private_key.pem file found.")
answer = None
while answer not in ['y', 'n']:
answer = input("Do you want me to create one for you? (Y/n)")
if not answer:
answer = 'y'
answer = answer.lower()[0]
if answer == 'n':
print("Sorry, can't do much for you then.")
exit
print("Generating private_key.pem")
Vapid().save_key('private_key.pem')
vapid = Vapid('private_key.pem')
if not os.path.exists('public_key.pem'):
print "No public_key.pem file found. You'll need this to access "
print "the developer dashboard."
answer = None
while answer not in ['y', 'n']:
answer = raw_input("Do you want me to create one for you? (Y/n)")
if not answer:
answer = 'y'
answer = answer.lower()[0]
if answer == 'y':
vapid.save_public_key('public_key.pem')
if args.gen or not os.path.exists('public_key.pem'):
if not args.gen:
print("No public_key.pem file found. You'll need this to access "
"the developer dashboard.")
answer = None
while answer not in ['y', 'n']:
answer = input("Do you want me to create one for you? (Y/n)")
if not answer:
answer = 'y'
answer = answer.lower()[0]
if answer == 'n':
print("Exiting...")
exit
print("Generating public_key.pem")
vapid.save_public_key('public_key.pem')
claim_file = args.sign
if claim_file:
if not os.path.exists(claim_file):
print "No %s file found." % claim_file
print """
print("No {} file found.".format(claim_file))
print("""
The claims file should be a JSON formatted file that holds the
information that describes you. There are three elements in the claims
file you'll need:
Expand All @@ -70,25 +78,27 @@ def main():
For example, a claims.json file could contain:
{"sub": "mailto:admin@example.com"}
"""
""")
exit
try:
claims = json.loads(open(claim_file).read())
result = vapid.sign(claims)
except Exception, exc:
print "Crap, something went wrong: %s", repr(exc)
except Exception as exc:
print("Crap, something went wrong: {}".format(repr(exc)))
raise exc

print "Include the following headers in your request:\n"
if args.json:
print(json.dumps(result))
return
print("Include the following headers in your request:\n")
for key, value in result.items():
print "%s: %s" % (key, value)
print "\n"
print("{}: {}\n".format(key, value))
print("\n")

token = args.validate
if token:
print "signed token for dashboard validation:\n"
print vapid.validate(token)
print "\n"
print("signed token for dashboard validation:\n")
print(vapid.validate(token))
print("\n")


if __name__ == '__main__':
Expand Down
20 changes: 12 additions & 8 deletions python/py_vapid/tests/test_vapid.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
-----END PUBLIC KEY-----
"""

T_PUBLIC_RAW = """EJwJZq_GN8jJbo1GGpyU70hmP2hbWAUpQFKDBy\
KB81yldJ9GTklBM5xqEwuPM7VuQcyiLDhvovthPIXx-gsQRQ==""".strip('=')
# this is a DER RAW key ('\x04' + 2 32 octet digits)
# Remember, this should have any padding stripped.
T_PUBLIC_RAW = (
"BBCcCWavxjfIyW6NRhqclO9IZj9oW1gFKUBSgwcigfNc"
"pXSfRk5JQTOcahMLjzO1bkHMoiw4b6L7YTyF8foLEEU"
).strip('=')


def setUp(self):
Expand Down Expand Up @@ -87,7 +91,7 @@ def test_save_key(self):
v.save_key("/tmp/p2")
os.unlink("/tmp/p2")

def test_save_public_key(self):
def test_same_public_key(self):
v = Vapid01()
v.generate_keys()
v.save_public_key("/tmp/p2")
Expand All @@ -108,11 +112,11 @@ def test_sign_01(self):
claims = {"aud": "example.com", "sub": "admin@example.com"}
result = v.sign(claims, "id=previous")
eq_(result['Crypto-Key'],
'id=previous,'
'p256ecdsa=' + T_PUBLIC_RAW)
items = jws.verify(result['Authorization'].split(' ')[1],
v.public_key,
algorithms=["ES256"])
'id=previous;p256ecdsa=' + T_PUBLIC_RAW)
items = jws.verify(
result['Authorization'].split(' ')[1],
binascii.b2a_base64(v.public_key.to_der()).decode('utf8'),
algorithms=["ES256"])
eq_(json.loads(items.decode('utf8')), claims)
result = v.sign(claims)
eq_(result['Crypto-Key'],
Expand Down
2 changes: 1 addition & 1 deletion python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ecdsa==0.13
python-jose==0.6.1
python-jose==1.3.2
Loading

0 comments on commit b747d2d

Please sign in to comment.