Skip to content

Commit ba088e1

Browse files
committed
feat: Add draft 02 support for python
https://tools.ietf.org/html/draft-ietf-webpush-vapid-02 fixes: #25
1 parent b53e60c commit ba088e1

File tree

3 files changed

+95
-25
lines changed

3 files changed

+95
-25
lines changed

python/py_vapid/__init__.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,25 @@
1111
import ecdsa
1212
from jose import jws
1313

14+
# Show compliance version. For earlier versions see previously tagged releases.
15+
VERSION = "VAPID-DRAFT-02/ECE-DRAFT-07"
16+
1417

1518
class VapidException(Exception):
1619
"""An exception wrapper for Vapid."""
1720
pass
1821

1922

20-
class Vapid(object):
21-
"""Minimal VAPID signature generation library. """
23+
class Vapid01(object):
24+
"""Minimal VAPID Draft 01 signature generation library.
25+
26+
https://tools.ietf.org/html/draft-ietf-webpush-vapid-01
27+
28+
"""
2229
_private_key = None
2330
_public_key = None
2431
_hasher = hashlib.sha256
32+
_schema = "WebPush"
2533

2634
def __init__(self, private_key_file=None, private_key=None):
2735
"""Initialize VAPID using an optional file containing a private key
@@ -137,6 +145,15 @@ def verify_token(self, validation_token, verification_token):
137145
return self.public_key.verify(hsig, validation_token,
138146
hashfunc=self._hasher)
139147

148+
def _base_sign(self, claims):
149+
if not claims.get('exp'):
150+
claims['exp'] = int(time.time()) + 86400
151+
if not claims.get('sub'):
152+
raise VapidException(
153+
"Missing 'sub' from claims. "
154+
"'sub' is your admin email as a mailto: link.")
155+
return claims
156+
140157
def sign(self, claims, crypto_key=None):
141158
"""Sign a set of claims.
142159
:param claims: JSON object containing the JWT claims to use.
@@ -149,12 +166,7 @@ def sign(self, claims, crypto_key=None):
149166
:rtype: dict
150167
151168
"""
152-
if not claims.get('exp'):
153-
claims['exp'] = int(time.time()) + 86400
154-
if not claims.get('sub'):
155-
raise VapidException(
156-
"Missing 'sub' from claims. "
157-
"'sub' is your admin email as a mailto: link.")
169+
claims = self._base_sign(claims)
158170
sig = jws.sign(claims, self.private_key, algorithm="ES256")
159171
pkey = 'p256ecdsa='
160172
pkey += base64.urlsafe_b64encode(self.public_key.to_string())
@@ -163,5 +175,32 @@ def sign(self, claims, crypto_key=None):
163175
else:
164176
crypto_key = pkey
165177

166-
return {"Authorization": "WebPush " + sig.strip('='),
178+
return {"Authorization": "{} {}".format(self._schema, sig.strip('=')),
167179
"Crypto-Key": crypto_key}
180+
181+
182+
class Vapid02(Vapid01):
183+
"""Minimal Vapid 02 signature generation library
184+
185+
https://tools.ietf.org/html/draft-ietf-webpush-vapid-02
186+
187+
"""
188+
_schema = "vapid"
189+
190+
def sign(self, claims, crypto_key=None):
191+
claims = self._base_sign(claims)
192+
sig = jws.sign(claims, self.private_key, algorithm="ES256")
193+
pkey = self.public_key.to_string()
194+
# Make sure that the key is properly prefixed.
195+
if len(pkey) == 64:
196+
pkey = '\04' + pkey
197+
return{
198+
"Authorization": "{schema} t={t},k={k}".format(
199+
schema=self._schema,
200+
t=sig,
201+
k=base64.urlsafe_b64encode(pkey).strip('=')
202+
)
203+
}
204+
205+
206+
Vapid = Vapid01

python/py_vapid/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
import os
77
import json
88

9-
from py_vapid import Vapid
9+
from py_vapid import Vapid01, Vapid02
1010

1111

1212
def main():
1313
parser = argparse.ArgumentParser(description="VAPID tool")
1414
parser.add_argument('--sign', '-s', help='claims file to sign')
1515
parser.add_argument('--validate', '-v', help='dashboard token to validate')
16+
parser.add_argument('--version2', '-2', help="use VAPID spec Draft-02",
17+
default=False, action="store_true")
18+
parser.add_argument('--version1', '-1', help="use VAPID spec Draft-01",
19+
default=True, action="store_true")
1620
args = parser.parse_args()
21+
Vapid = Vapid01
22+
if args.version2:
23+
Vapid = Vapid02
1724
if not os.path.exists('private_key.pem'):
1825
print "No private_key.pem file found."
1926
answer = None

python/py_vapid/tests/test_vapid.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mock import patch
88

99
from jose import jws
10-
from py_vapid import Vapid, VapidException
10+
from py_vapid import Vapid01, Vapid02, VapidException
1111

1212
T_DER = """
1313
MHcCAQEEIPeN1iAipHbt8+/KZ2NIF8NeN24jqAmnMLFZEMocY8RboAoGCCqGSM49
@@ -43,54 +43,57 @@ def tearDown(self):
4343

4444
class VapidTestCase(unittest.TestCase):
4545
def test_init(self):
46-
v1 = Vapid(private_key_file="/tmp/private")
46+
v1 = Vapid01(private_key_file="/tmp/private")
4747
eq_(v1.private_key.to_pem(), T_PRIVATE)
4848
eq_(v1.public_key.to_pem(), T_PUBLIC)
49-
v2 = Vapid(private_key=T_PRIVATE)
49+
v2 = Vapid01(private_key=T_PRIVATE)
5050
eq_(v2.private_key.to_pem(), T_PRIVATE)
5151
eq_(v2.public_key.to_pem(), T_PUBLIC)
52-
v3 = Vapid(private_key=T_DER)
52+
v3 = Vapid01(private_key=T_DER)
5353
eq_(v3.private_key.to_pem(), T_PRIVATE)
5454
eq_(v3.public_key.to_pem(), T_PUBLIC)
5555
no_exist = '/tmp/not_exist'
56-
Vapid(private_key_file=no_exist)
56+
Vapid01(private_key_file=no_exist)
5757
ok_(os.path.isfile(no_exist))
5858
os.unlink(no_exist)
5959

60+
def repad(self, data):
61+
return data + b"===="[:len(data) % 4]
62+
6063
@patch("ecdsa.SigningKey.from_pem", side_effect=Exception)
6164
def test_init_bad_priv(self, mm):
6265
self.assertRaises(Exception,
63-
Vapid,
66+
Vapid01,
6467
private_key_file="/tmp/private")
6568

6669
def test_private(self):
67-
v = Vapid()
70+
v = Vapid01()
6871
self.assertRaises(VapidException, lambda x=None: v.private_key)
6972

7073
def test_public(self):
71-
v = Vapid()
74+
v = Vapid01()
7275

7376
self.assertRaises(VapidException, lambda x=None: v.public_key)
7477

7578
def test_gen_key(self):
76-
v = Vapid()
79+
v = Vapid01()
7780
v.generate_keys()
7881
ok_(v.public_key)
7982
ok_(v.private_key)
8083

8184
def test_save_key(self):
82-
v = Vapid()
85+
v = Vapid01()
8386
v.save_key("/tmp/p2")
8487
os.unlink("/tmp/p2")
8588

8689
def test_save_public_key(self):
87-
v = Vapid()
90+
v = Vapid01()
8891
v.generate_keys()
8992
v.save_public_key("/tmp/p2")
9093
os.unlink("/tmp/p2")
9194

9295
def test_validate(self):
93-
v = Vapid("/tmp/private")
96+
v = Vapid01("/tmp/private")
9497
msg = "foobar"
9598
vtoken = v.validate(msg)
9699
ok_(v.public_key.verify(base64.urlsafe_b64decode(vtoken),
@@ -99,8 +102,8 @@ def test_validate(self):
99102
# test verify
100103
ok_(v.verify_token(msg, vtoken))
101104

102-
def test_sign(self):
103-
v = Vapid("/tmp/private")
105+
def test_sign_01(self):
106+
v = Vapid01("/tmp/private")
104107
claims = {"aud": "example.com", "sub": "[email protected]"}
105108
result = v.sign(claims, "id=previous")
106109
eq_(result['Crypto-Key'],
@@ -114,8 +117,29 @@ def test_sign(self):
114117
eq_(result['Crypto-Key'],
115118
'p256ecdsa=' + T_PUBLIC_RAW)
116119

120+
def test_sign_02(self):
121+
v = Vapid02("/tmp/private")
122+
claims = {"aud": "example.com",
123+
124+
"foo": "extra value"}
125+
result = v.sign(claims, "id=previous")
126+
auth = result['Authorization']
127+
eq_(auth[:6], 'vapid ')
128+
ok_(' t=' in auth)
129+
ok_(',k=' in auth)
130+
parts = auth[6:].split(',')
131+
eq_(len(parts), 2)
132+
t_val = json.loads(base64.urlsafe_b64decode(
133+
self.repad(parts[0][2:].split('.')[1])
134+
))
135+
k_val = base64.urlsafe_b64decode(self.repad(parts[1][2:]))
136+
eq_(k_val[0], "\04")
137+
eq_(len(k_val), 65)
138+
for k in claims:
139+
eq_(t_val[k], claims[k])
140+
117141
def test_bad_sign(self):
118-
v = Vapid("/tmp/private")
142+
v = Vapid01("/tmp/private")
119143
self.assertRaises(VapidException,
120144
v.sign,
121145
{'aud': "p.example.com"})

0 commit comments

Comments
 (0)