Skip to content

Commit e991f98

Browse files
authored
Merge pull request #50 from web-push-libs/bug/49a
bug: Several important patches were lost. Reapplying
2 parents acf3de6 + 3d7b554 commit e991f98

File tree

5 files changed

+62
-62
lines changed

5 files changed

+62
-62
lines changed

python/py_vapid/__init__.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import re
1010

1111
from cryptography.hazmat.backends import default_backend
12-
from cryptography.hazmat.primitives.asymmetric import ec
12+
from cryptography.hazmat.primitives.asymmetric import ec, utils as ecutils
1313
from cryptography.hazmat.primitives import serialization
1414

1515
from cryptography.hazmat.primitives import hashes
@@ -63,6 +63,16 @@ def from_raw(cls, private_raw):
6363
backend=default_backend())
6464
return cls(key)
6565

66+
@classmethod
67+
def from_raw_public(cls, public_raw):
68+
key = ec.EllipticCurvePublicNumbers.from_encoded_point(
69+
curve=ec.SECP256R1(),
70+
data=b64urldecode(public_raw)
71+
).public_key(default_backend())
72+
ss = cls()
73+
ss._public_key = key
74+
return ss
75+
6676
@classmethod
6777
def from_pem(cls, private_key):
6878
"""Initialize VAPID using a private key in PEM format.
@@ -113,6 +123,16 @@ def from_file(cls, private_key_file=None):
113123
logging.error("Could not open private key file: %s", repr(exc))
114124
raise VapidException(exc)
115125

126+
@classmethod
127+
def verify(cls, key, auth):
128+
# TODO: add v2 validation
129+
tokens = auth.rsplit(' ', 1)[1].rsplit('.', 1)
130+
kp = cls().from_raw_public(key.encode())
131+
return kp.verify_token(
132+
validation_token=tokens[0].encode(),
133+
verification_token=tokens[1]
134+
)
135+
116136
@property
117137
def private_key(self):
118138
"""The VAPID private ECDSA key"""
@@ -184,21 +204,6 @@ def save_public_key(self, key_file):
184204
file.write(self.public_pem())
185205
file.close()
186206

187-
def validate(self, validation_token):
188-
"""Sign a Valdiation token from the dashboard
189-
190-
:param validation_token: Short validation token from the dev dashboard
191-
:type validation_token: str
192-
:returns: corresponding token for key verification
193-
:rtype: str
194-
195-
"""
196-
sig = self.private_key.sign(
197-
validation_token,
198-
signature_algorithm=ec.ECDSA(hashes.SHA256()))
199-
verification_token = b64urlencode(sig)
200-
return verification_token
201-
202207
def verify_token(self, validation_token, verification_token):
203208
"""Internally used to verify the verification token is correct.
204209
@@ -211,8 +216,10 @@ def verify_token(self, validation_token, verification_token):
211216
212217
"""
213218
hsig = b64urldecode(verification_token.encode('utf8'))
219+
r = int(binascii.hexlify(hsig[:32]), 16)
220+
s = int(binascii.hexlify(hsig[32:]), 16)
214221
return self.public_key.verify(
215-
hsig,
222+
ecutils.encode_dss_signature(r, s),
216223
validation_token,
217224
signature_algorithm=ec.ECDSA(hashes.SHA256())
218225
)

python/py_vapid/jwt.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
from cryptography.hazmat.primitives.asymmetric import ec, utils
77
from cryptography.hazmat.primitives import hashes
88

9-
from py_vapid.utils import b64urldecode, b64urlencode
9+
from py_vapid.utils import b64urldecode, b64urlencode, num_to_bytes
1010

1111

1212
def extract_signature(auth):
13-
"""Fix the JWT auth token
14-
15-
convert a ecdsa integer pair into an OpenSSL DER pair.
13+
"""Extracts the payload and signature from a JWT, converting from RFC7518
14+
to RFC 3279
1615
1716
:param auth: A JWT Authorization Token.
1817
:type auth: str
@@ -23,7 +22,7 @@ def extract_signature(auth):
2322
payload, asig = auth.encode('utf8').rsplit(b'.', 1)
2423
sig = b64urldecode(asig)
2524
if len(sig) != 64:
26-
return payload, sig
25+
raise InvalidSignature()
2726

2827
encoded = utils.encode_dss_signature(
2928
s=int(binascii.hexlify(sig[32:]), 16),
@@ -35,8 +34,6 @@ def extract_signature(auth):
3534
def decode(token, key):
3635
"""Decode a web token into an assertion dictionary
3736
38-
This attempts to rectify both ecdsa and openssl generated signatures.
39-
4037
:param token: VAPID auth token
4138
:type token: str
4239
:param key: bitarray containing the public key
@@ -80,9 +77,12 @@ def sign(claims, key):
8077
8178
"""
8279
header = b64urlencode(b"""{"typ":"JWT","alg":"ES256"}""")
80+
# Unfortunately, chrome seems to require the claims to be sorted.
8381
claims = b64urlencode(json.dumps(claims,
84-
separators=(',', ':')).encode('utf8'))
82+
separators=(',', ':'),
83+
sort_keys=True).encode('utf8'))
8584
token = "{}.{}".format(header, claims)
8685
rsig = key.sign(token.encode('utf8'), ec.ECDSA(hashes.SHA256()))
87-
sig = b64urlencode(rsig)
86+
(r, s) = utils.decode_dss_signature(rsig)
87+
sig = b64urlencode(num_to_bytes(r) + num_to_bytes(s))
8888
return "{}.{}".format(token, sig)

python/py_vapid/tests/test_vapid.py

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
from nose.tools import eq_, ok_
77
from mock import patch, Mock
88

9-
from cryptography.hazmat.primitives.asymmetric import ec, utils
10-
from cryptography.hazmat.primitives import hashes
11-
129
from py_vapid import Vapid01, Vapid02, VapidException
1310
from py_vapid.jwt import decode
14-
from py_vapid.utils import b64urldecode
1511

1612
# This is a private key in DER form.
1713
T_DER = """
@@ -126,17 +122,6 @@ def test_from_raw(self):
126122
v = Vapid01.from_raw(T_RAW)
127123
self.check_keys(v)
128124

129-
def test_validate(self):
130-
v = Vapid01.from_file("/tmp/private")
131-
msg = "foobar".encode('utf8')
132-
vtoken = v.validate(msg)
133-
ok_(v.public_key.verify(
134-
base64.urlsafe_b64decode(self.repad(vtoken).encode()),
135-
msg,
136-
ec.ECDSA(hashes.SHA256())))
137-
# test verify
138-
ok_(v.verify_token(msg, vtoken))
139-
140125
def test_sign_01(self):
141126
v = Vapid01.from_file("/tmp/private")
142127
claims = {"aud": "https://example.com",
@@ -152,6 +137,12 @@ def test_sign_01(self):
152137
result = v.sign(claims)
153138
eq_(result['Crypto-Key'],
154139
'p256ecdsa=' + T_PUBLIC_RAW.decode('utf8'))
140+
# Verify using the same function as Integration
141+
# this should ensure that the r,s sign values are correctly formed
142+
ok_(Vapid01.verify(
143+
key=result['Crypto-Key'].split('=')[1],
144+
auth=result['Authorization']
145+
))
155146

156147
def test_sign_02(self):
157148
v = Vapid02.from_file("/tmp/private")
@@ -174,26 +165,17 @@ def test_sign_02(self):
174165
for k in claims:
175166
eq_(t_val[k], claims[k])
176167

177-
def test_alt_sign(self):
178-
"""ecdsa uses a raw key pair to sign, openssl uses a DER."""
179-
v = Vapid01.from_file("/tmp/private")
180-
claims = {"aud": "https://example.com",
181-
"sub": "mailto:[email protected]",
182-
"foo": "extra value"}
183-
# Get a signed token.
184-
result = v.sign(claims)
185-
# Convert the dss into raw.
186-
auth, sig = result.get('Authorization').split(' ')[1].rsplit('.', 1)
187-
ss = utils.decode_dss_signature(b64urldecode(sig.encode('utf8')))
188-
new_sig = binascii.b2a_base64(
189-
binascii.unhexlify("%064x%064x" % ss)
190-
).strip().strip(b'=').decode()
191-
new_auth = auth + '.' + new_sig
192-
# phew, all that done, now check
193-
pkey = result.get("Crypto-Key").split('=')[1]
194-
items = decode(new_auth, pkey)
195-
196-
eq_(items, claims)
168+
def test_integration(self):
169+
# These values were taken from a test page. DO NOT ALTER!
170+
key = ("BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foI"
171+
"iBHXRdJI2Qhumhf6_LFTeZaNndIo")
172+
173+
auth = ("WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJod"
174+
"HRwczovL3VwZGF0ZXMucHVzaC5zZXJ2aWNlcy5tb3ppbGxhLmNvbSIsImV"
175+
"4cCI6MTQ5NDY3MTQ3MCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlb"
176+
"W9AZ2F1bnRmYWNlLmNvLnVrIn0.LqPi86T-HJ71TXHAYFptZEHD7Wlfjcc"
177+
"4u5jYZ17WpqOlqDcW-5Wtx3x1OgYX19alhJ9oLumlS2VzEvNioZolQA")
178+
ok_(Vapid01.verify(key=key, auth=auth))
197179

198180
def test_bad_sign(self):
199181
v = Vapid01.from_file("/tmp/private")

python/py_vapid/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import binascii
23

34

45
def b64urldecode(data):
@@ -23,3 +24,13 @@ def b64urlencode(data):
2324
2425
"""
2526
return base64.urlsafe_b64encode(data).replace(b'=', b'').decode('utf8')
27+
28+
29+
def num_to_bytes(n):
30+
"""Returns the byte representation of an integer, in big-endian order.
31+
:param n: The integer to encode.
32+
:type n: int
33+
:returns bytes
34+
"""
35+
h = '%x' % n
36+
return binascii.unhexlify('0' * (len(h) % 2) + h)

python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from setuptools import setup, find_packages
55

6-
__version__ = "1.2.2"
6+
__version__ = "1.2.3"
77

88

99
def read_from(file):

0 commit comments

Comments
 (0)