Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
language: python
python:
- "2.7"
- "3.5"
install:
- cd python
- pip install -r requirements.txt
Expand Down
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're moving the .md -> .rst but forgot to delete the .md?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it as well. There are a lot of apps that like .md more than .rst, and it's a simple script to convert. (In fact, it's part of my publish script pandoc --from=markdown --to=rst --output README.rst README.md ) I'm ok with dropping the .rst from the repo, but since it's part of the python packaging, I'd rather keep it.

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:[email protected]","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:[email protected]`" or a
specific address like "`mailto:[email protected]`".

***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:[email protected]","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:[email protected]``"
or a specific address like "``mailto:[email protected]``".

***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:[email protected]"}
"""
""")
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
13 changes: 8 additions & 5 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,8 +112,7 @@ def test_sign_01(self):
claims = {"aud": "example.com", "sub": "[email protected]"}
result = v.sign(claims, "id=previous")
eq_(result['Crypto-Key'],
'id=previous,'
'p256ecdsa=' + T_PUBLIC_RAW)
'id=previous;p256ecdsa=' + T_PUBLIC_RAW)
items = jws.verify(result['Authorization'].split(' ')[1],
v.public_key,
algorithms=["ES256"])
Expand Down
Loading