Skip to content

Commit 5704e0f

Browse files
authored
Merge pull request #31 from web-push-libs/feat/3.4
Feat: 3.4 support and JSON dump
2 parents 2b3d084 + 48ac1b9 commit 5704e0f

File tree

9 files changed

+214
-56
lines changed

9 files changed

+214
-56
lines changed

Diff for: .travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
language: python
22
python:
33
- "2.7"
4+
- "3.5"
45
install:
56
- cd python
67
- pip install -r requirements.txt

Diff for: python/MANIFEST.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ include *.md
22
include *.txt
33
include setup.*
44
include LICENSE
5-
recursive-include py_vapid
5+
recursive-include py_vapid *.py

Diff for: python/README.md

+54-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,50 @@ This minimal library contains the minimal set of functions you need to
44
generate a VAPID key set and get the headers you'll need to sign a
55
WebPush subscription update.
66

7-
This can either be installed as a library or used as a stand along
8-
app.
7+
VAPID is a voluntary standard for WebPush subscription providers
8+
(sites that send WebPush updates to remote customers) to self-identify
9+
to Push Servers (the servers that convey the push notifications).
10+
11+
The VAPID "claims" are a set of JSON keys and values. There are two
12+
required fields, one semi-optional and several optional additional
13+
fields.
14+
15+
At a minimum a VAPID claim set should look like:
16+
```
17+
{"sub":"mailto:[email protected]","aud":"https://PushServerURL","exp":"ExpirationTimestamp"}
18+
```
19+
A few notes:
20+
21+
***sub*** is the email address you wish to have on record for this
22+
request, prefixed with "`mailto:`". If things go wrong, this is the
23+
email that will be used to contact you (for instance). This can be a
24+
general delivery address like "`mailto:[email protected]`" or a
25+
specific address like "`mailto:[email protected]`".
26+
27+
***aud*** is the audience for the VAPID. This it the host path you use to
28+
send subscription endpoints and generally coincides with the
29+
`endpoint` specified in the Subscription Info block.
30+
31+
As example, if a WebPush subscription info contains:
32+
`{"endpoint": "https://push.example.com:8012/v1/push/...", ...}`
33+
34+
then the `aud` would be "`https://push.example.com:8012/`"
35+
36+
While some Push Services consider this an optional field, others may
37+
be stricter.
38+
39+
***exp*** This is the UTC timestamp for when this VAPID request will
40+
expire. The maximum period is 24 hours. Setting a shorter period can
41+
prevent "replay" attacks. Setting a longer period allows you to reuse
42+
headers for multiple sends (e.g. if you're sending hundreds of updates
43+
within an hour or so.) If no `exp` is included, one that will expire
44+
in 24 hours will be auto-generated for you.
45+
46+
Claims should be stored in a JSON compatible file. In the examples
47+
below, we've stored the claims into a file named `claims.json`.
48+
49+
py_vapid can either be installed as a library or used as a stand along
50+
app, `bin/vapid`.
951

1052
## App Installation
1153

@@ -15,18 +57,24 @@ Then run
1557
```
1658
bin/pip install -r requirements.txt
1759
18-
bin/python setup.py`install
60+
bin/python setup.py install
1961
```
2062
## App Usage
2163

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

25-
`bin/vapid --sign _claims.json_` will generate a set of HTTP headers
67+
`bin/vapid -gen` can be used to generate a new set of public and
68+
private key PEM files. These will overwrite the contents of
69+
`private_key.pem` and `public_key.pem`.
70+
71+
`bin/vapid --sign claims.json` will generate a set of HTTP headers
2672
from a JSON formatted claims file. A sample `claims.json` is included
2773
with this distribution.
2874

29-
`bin/vapid --validate _token_` will generate a token response for the
30-
Mozilla WebPush dashboard.
75+
`bin/vapid --sign claims.json --json` will output the headers in
76+
JSON format, which may be useful for other programs.
77+
78+
See `bin/vapid -h` for all options and commands.
3179

3280

Diff for: python/README.rst

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
Easy VAPID generation
2+
=====================
3+
4+
This minimal library contains the minimal set of functions you need to
5+
generate a VAPID key set and get the headers you'll need to sign a
6+
WebPush subscription update.
7+
8+
VAPID is a voluntary standard for WebPush subscription providers (sites
9+
that send WebPush updates to remote customers) to self-identify to Push
10+
Servers (the servers that convey the push notifications).
11+
12+
The VAPID "claims" are a set of JSON keys and values. There are two
13+
required fields, one semi-optional and several optional additional
14+
fields.
15+
16+
At a minimum a VAPID claim set should look like:
17+
18+
::
19+
20+
{"sub":"mailto:[email protected]","aud":"https://PushServerURL","exp":"ExpirationTimestamp"}
21+
22+
A few notes:
23+
24+
***sub*** is the email address you wish to have on record for this
25+
request, prefixed with "``mailto:``". If things go wrong, this is the
26+
email that will be used to contact you (for instance). This can be a
27+
general delivery address like "``mailto:[email protected]``"
28+
or a specific address like "``mailto:[email protected]``".
29+
30+
***aud*** is the audience for the VAPID. This it the host path you use
31+
to send subscription endpoints and generally coincides with the
32+
``endpoint`` specified in the Subscription Info block.
33+
34+
As example, if a WebPush subscription info contains:
35+
``{"endpoint": "https://push.example.com:8012/v1/push/...", ...}``
36+
37+
then the ``aud`` would be "``https://push.example.com:8012/``"
38+
39+
While some Push Services consider this an optional field, others may be
40+
stricter.
41+
42+
***exp*** This is the UTC timestamp for when this VAPID request will
43+
expire. The maximum period is 24 hours. Setting a shorter period can
44+
prevent "replay" attacks. Setting a longer period allows you to reuse
45+
headers for multiple sends (e.g. if you're sending hundreds of updates
46+
within an hour or so.) If no ``exp`` is included, one that will expire
47+
in 24 hours will be auto-generated for you.
48+
49+
Claims should be stored in a JSON compatible file. In the examples
50+
below, we've stored the claims into a file named ``claims.json``.
51+
52+
py\_vapid can either be installed as a library or used as a stand along
53+
app, ``bin/vapid``.
54+
55+
App Installation
56+
----------------
57+
58+
You'll need ``python virtualenv`` Run that in the current directory.
59+
60+
Then run
61+
62+
::
63+
64+
bin/pip install -r requirements.txt
65+
66+
bin/python setup.py install
67+
68+
App Usage
69+
---------
70+
71+
Run by itself, ``bin/vapid`` will check and optionally create the
72+
public\_key.pem and private\_key.pem files.
73+
74+
``bin/vapid -gen`` can be used to generate a new set of public and
75+
private key PEM files. These will overwrite the contents of
76+
``private_key.pem`` and ``public_key.pem``.
77+
78+
``bin/vapid --sign claims.json`` will generate a set of HTTP headers
79+
from a JSON formatted claims file. A sample ``claims.json`` is included
80+
with this distribution.
81+
82+
``bin/vapid --sign claims.json --json`` will output the headers in JSON
83+
format, which may be useful for other programs.
84+
85+
See ``bin/vapid -h`` for all options and commands.

Diff for: python/py_vapid/__init__.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ def validate(self, validation_token):
127127
:rtype: str
128128
129129
"""
130-
sig = self.private_key.sign(validation_token,
130+
sig = self.private_key.sign(
131+
validation_token,
131132
hashfunc=self._hasher)
132133
verification_token = base64.urlsafe_b64encode(sig)
133134
return verification_token
@@ -149,7 +150,7 @@ def verify_token(self, validation_token, verification_token):
149150

150151
def _base_sign(self, claims):
151152
if not claims.get('exp'):
152-
claims['exp'] = int(time.time()) + 86400
153+
claims['exp'] = str(int(time.time()) + 86400)
153154
if not claims.get('sub'):
154155
raise VapidException(
155156
"Missing 'sub' from claims. "
@@ -176,9 +177,12 @@ def sign(self, claims, crypto_key=None):
176177
claims = self._base_sign(claims)
177178
sig = jws.sign(claims, self.private_key, algorithm="ES256")
178179
pkey = 'p256ecdsa='
179-
pkey += self.encode(self.public_key.to_string())
180+
pubkey = self.public_key.to_string()
181+
if len(pubkey) == 64:
182+
pubkey = b'\04' + pubkey
183+
pkey += self.encode(pubkey)
180184
if crypto_key:
181-
crypto_key = crypto_key + ',' + pkey
185+
crypto_key = crypto_key + ';' + pkey
182186
else:
183187
crypto_key = pkey
184188

Diff for: python/py_vapid/main.py

+46-36
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,54 @@
1212
def main():
1313
parser = argparse.ArgumentParser(description="VAPID tool")
1414
parser.add_argument('--sign', '-s', help='claims file to sign')
15+
parser.add_argument('--gen', '-g', help='generate new key pairs',
16+
default=False, action="store_true")
1517
parser.add_argument('--validate', '-v', help='dashboard token to validate')
1618
parser.add_argument('--version2', '-2', help="use VAPID spec Draft-02",
1719
default=False, action="store_true")
1820
parser.add_argument('--version1', '-1', help="use VAPID spec Draft-01",
1921
default=True, action="store_true")
22+
parser.add_argument('--json', help="dump as json",
23+
default=False, action="store_true")
2024
args = parser.parse_args()
2125
Vapid = Vapid01
2226
if args.version2:
2327
Vapid = Vapid02
24-
if not os.path.exists('private_key.pem'):
25-
print "No private_key.pem file found."
26-
answer = None
27-
while answer not in ['y', 'n']:
28-
answer = raw_input("Do you want me to create one for you? (Y/n)")
29-
if not answer:
30-
answer = 'y'
31-
answer = answer.lower()[0]
32-
if answer == 'n':
33-
print "Sorry, can't do much for you then."
34-
exit
35-
if answer == 'y':
36-
break
28+
if args.gen or not os.path.exists('private_key.pem'):
29+
if not args.gen:
30+
print("No private_key.pem file found.")
31+
answer = None
32+
while answer not in ['y', 'n']:
33+
answer = input("Do you want me to create one for you? (Y/n)")
34+
if not answer:
35+
answer = 'y'
36+
answer = answer.lower()[0]
37+
if answer == 'n':
38+
print ("Sorry, can't do much for you then.")
39+
exit
40+
print("Generating private_key.pem")
3741
Vapid().save_key('private_key.pem')
3842
vapid = Vapid('private_key.pem')
39-
if not os.path.exists('public_key.pem'):
40-
print "No public_key.pem file found. You'll need this to access "
41-
print "the developer dashboard."
42-
answer = None
43-
while answer not in ['y', 'n']:
44-
answer = raw_input("Do you want me to create one for you? (Y/n)")
45-
if not answer:
46-
answer = 'y'
47-
answer = answer.lower()[0]
48-
if answer == 'y':
49-
vapid.save_public_key('public_key.pem')
43+
if args.gen or not os.path.exists('public_key.pem'):
44+
if not args.gen:
45+
print("No public_key.pem file found. You'll need this to access "
46+
"the developer dashboard.")
47+
answer = None
48+
while answer not in ['y', 'n']:
49+
answer = input("Do you want me to create one for you? (Y/n)")
50+
if not answer:
51+
answer = 'y'
52+
answer = answer.lower()[0]
53+
if answer == 'n':
54+
print ("Exiting...")
55+
exit
56+
print("Generating public_key.pem")
57+
vapid.save_public_key('public_key.pem')
5058
claim_file = args.sign
5159
if claim_file:
5260
if not os.path.exists(claim_file):
53-
print "No %s file found." % claim_file
54-
print """
61+
print("No {} file found.".format(claim_file))
62+
print("""
5563
The claims file should be a JSON formatted file that holds the
5664
information that describes you. There are three elements in the claims
5765
file you'll need:
@@ -70,25 +78,27 @@ def main():
7078
For example, a claims.json file could contain:
7179
7280
{"sub": "mailto:[email protected]"}
73-
"""
81+
""")
7482
exit
7583
try:
7684
claims = json.loads(open(claim_file).read())
7785
result = vapid.sign(claims)
78-
except Exception, exc:
79-
print "Crap, something went wrong: %s", repr(exc)
86+
except Exception as exc:
87+
print("Crap, something went wrong: {}".format(repr(exc)))
8088
raise exc
81-
82-
print "Include the following headers in your request:\n"
89+
if args.json:
90+
print(json.dumps(result))
91+
return
92+
print("Include the following headers in your request:\n")
8393
for key, value in result.items():
84-
print "%s: %s" % (key, value)
85-
print "\n"
94+
print("{}: {}\n".format(key, value))
95+
print("\n")
8696

8797
token = args.validate
8898
if token:
89-
print "signed token for dashboard validation:\n"
90-
print vapid.validate(token)
91-
print "\n"
99+
print("signed token for dashboard validation:\n")
100+
print(vapid.validate(token))
101+
print("\n")
92102

93103

94104
if __name__ == '__main__':

Diff for: python/py_vapid/tests/test_vapid.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424
-----END PUBLIC KEY-----
2525
"""
2626

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

3034

3135
def setUp(self):
@@ -87,7 +91,7 @@ def test_save_key(self):
8791
v.save_key("/tmp/p2")
8892
os.unlink("/tmp/p2")
8993

90-
def test_save_public_key(self):
94+
def test_same_public_key(self):
9195
v = Vapid01()
9296
v.generate_keys()
9397
v.save_public_key("/tmp/p2")
@@ -108,8 +112,7 @@ def test_sign_01(self):
108112
claims = {"aud": "example.com", "sub": "[email protected]"}
109113
result = v.sign(claims, "id=previous")
110114
eq_(result['Crypto-Key'],
111-
'id=previous,'
112-
'p256ecdsa=' + T_PUBLIC_RAW)
115+
'id=previous;p256ecdsa=' + T_PUBLIC_RAW)
113116
items = jws.verify(result['Authorization'].split(' ')[1],
114117
v.public_key,
115118
algorithms=["ES256"])

0 commit comments

Comments
 (0)