Skip to content

Commit 9e155d8

Browse files
authored
Merge pull request #26 from web-push-libs/feat/v02
Add support for VAPID draft 02
2 parents b53e60c + 5b0b9eb commit 9e155d8

File tree

6 files changed

+216
-75
lines changed

6 files changed

+216
-75
lines changed

js/common.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class MozCommon {
2727
fromUrlBase64(data) {
2828
/* return a binary array from a URL safe base64 string
2929
*/
30-
return atob((data + "====".substr(data.length % 4))
30+
return atob(data
3131
.replace(/\-/g, "+")
3232
.replace(/\_/g, "/"));
3333
}

js/index.html

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ <h1>VAPID verification</h1>
2222
should be readable by many encryption libraries.</p>
2323
</div>
2424
<div id="inputs" class="section">
25+
<div id="version">
26+
<label for="version"><input type="radio" name="version" value="1" checked>VAPID Draft spec-01</label>
27+
<label for="version"><input type="radio" name="version" value="2">VAPID Draft spec-02</label>
28+
</div>
2529
<a name="headers"><h2>Headers</h2></a>
2630
<p>The headers are sent with subscription updates. They provide the
2731
site information to associate with this feed. PLEASE NOTE: Your private
@@ -35,11 +39,11 @@ <h1>VAPID verification</h1>
3539
POST update.</div>
3640
<textarea name="auth" placeholder="Bearer abCDef..."></textarea>
3741
<label for="crypt">Crypto-Key Header:</label>
38-
<div class="description">This is your VAPID public key. This is included
42+
<div class="description">This is included
3943
as part of the <code>Crypto-Key</code> header, which is included
4044
as part of the subscription POST update. <code>Crypto-Key</code>
4145
may contain more than one part. Each part should be separated by a
42-
comma (",")</div>
46+
comma (",") (NOTE: For Draft-02, this header is not required)</div>
4347
<textarea name="crypt" placeholder="p256ecdsa=abCDef.."></textarea>
4448
<div class="control">
4549
<label for="publicKey">Public Key:</label>
@@ -116,6 +120,13 @@ <h3>Claims JSON object:</h3>
116120
}
117121
}
118122

123+
function vapid_version() {
124+
if (document.getElementById('version').getElementsByTagName('input')[0].checked) {
125+
return new VapidToken01();
126+
}
127+
return new VapidToken02();
128+
}
129+
119130
function error(ex=null, msg=null, clear=false) {
120131
let er = document.getElementById("err");
121132
if (clear) {
@@ -215,6 +226,7 @@ <h3>Claims JSON object:</h3>
215226

216227
function gen(){
217228
// clear the headers
229+
let vapidToken = vapid_version();
218230
for (h of document.getElementById("inputs").getElementsByTagName("textarea")) {
219231
h.value = "";
220232
h.classList.remove("updated");
@@ -229,15 +241,17 @@ <h3>Claims JSON object:</h3>
229241
console.debug(sc);
230242
rclaims.innerHTML = sc;
231243
rclaims.classList.add("updated");
232-
vapid.generate_keys().then(x => {
233-
vapid.sign(claims)
244+
vapidToken.generate_keys().then(x => {
245+
vapidToken.sign(claims)
234246
.then(k => {
235247
let auth = document.getElementsByName("auth")[0];
236248
auth.value = k.authorization;
237249
auth.classList.add('updated');
238250
let crypt = document.getElementsByName("crypt")[0];
239-
crypt.value = k["crypto-key"];
240-
crypt.classList.add('updated');
251+
if (k["crypto-key"]) {
252+
crypt.value = k["crypto-key"];
253+
crypt.classList.add('updated');
254+
}
241255
let pk = document.getElementsByName("publicKey")[0];
242256
// Public Key is the crypto key minus the 'p256ecdsa='
243257
pk.innerHTML = k.publicKey;
@@ -258,6 +272,7 @@ <h3>Claims JSON object:</h3>
258272
}
259273

260274
function check(){
275+
let vapidToken = vapid_version();
261276
try {
262277
// clear claims
263278
for (let item of document
@@ -274,14 +289,14 @@ <h3>Claims JSON object:</h3>
274289
let token = fetchAuth();
275290
let public_key = fetchCrypt();
276291
if ((token == null) && (pubic_key == null)) {
277-
if (token == null){
278-
error(null, err_strs.enus.BAD_AUTH_HE);
279-
return
280-
}
281-
failure(null, err_strs.enus.BAD_CRYP_HE);
292+
if (token == null) {
293+
error(null, err_strs.enus.BAD_AUTH_HE);
282294
return
295+
}
296+
failure(null, err_strs.enus.BAD_CRYP_HE);
297+
return
283298
}
284-
vapid.verify(token, public_key)
299+
vapidToken.verify(token, public_key)
285300
.then(k => success(k))
286301
.catch(err => error(err, err_strs.enus.BAD_HEADERS));
287302
} catch (e) {
@@ -293,8 +308,6 @@ <h3>Claims JSON object:</h3>
293308
document.getElementById("gen").addEventListener("click", gen);
294309
document.getElementById('vapid_exp').value = parseInt(Date.now()*.001) + 86400;
295310

296-
var vapid = new VapidToken();
297-
var mzcc = new MozCommon();
298311

299312
</script>
300313
</body>

js/vapid.js

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ try {
1414
var webCrypto = window.crypto.subtle;
1515
}
1616

17-
class VapidToken {
17+
class VapidCore {
1818
constructor(aud, sub, exp, lang, mzcc) {
1919
/* Construct a base VAPID token.
2020
*
@@ -131,16 +131,14 @@ class VapidToken {
131131
key_ops: ["verify"],
132132
kty: "EC",
133133
x: x,
134-
y, y
134+
y: y,
135135
};
136136

137137
return webCrypto.importKey('jwk', jwk, 'ECDSA', true, ["verify"])
138138
.then(k => this._public_key = k)
139139
}
140140

141-
142-
143-
sign(claims) {
141+
_sign(claims) {
144142
/* Sign a claims object and return the headers that can be used to
145143
* decrypt the string.
146144
*
@@ -186,9 +184,8 @@ class VapidToken {
186184
return this.export_public_raw()
187185
.then( pubKey => {
188186
return {
189-
authorization: "WebPush " + content + "." + sig,
190-
"crypto-key": "p256ecdsa=" + pubKey,
191-
publicKey: pubKey,
187+
jwt: content + "." + sig,
188+
pubkey: pubKey,
192189
}
193190
})
194191
})
@@ -197,7 +194,7 @@ class VapidToken {
197194
})
198195
}
199196

200-
verify(token, public_key=null) {
197+
_verify(token) {
201198
/* Verify a VAPID token.
202199
*
203200
* Token is the Authorization Header, Public Key is the Crypto-Key
@@ -207,33 +204,8 @@ class VapidToken {
207204
*/
208205

209206
// Ideally, just the bearer token, Cheat a little to be nice to the dev.
210-
scheme = token.toLowerCase().split(" ")[0]
211-
if (scheme == "bearer" || scheme == "webpush") {
212-
token = token.split(" ")[1];
213-
}
214-
215-
// Again, ideally, just the p256ecdsa token.
216-
if (public_key != null) {
217-
218-
if (public_key.search('p256ecdsa') > -1) {
219-
let sc = /p256ecdsa=([^;,]+)/i;
220-
public_key = sc.exec(public_key)[1];
221-
}
222-
223-
// If there's no public key already defined, load the public_key
224-
// and try again.
225-
return this.import_public_raw(public_key)
226-
.then(key => {
227-
this._public_key = key;
228-
return this.verify(token);
229-
})
230-
.catch(err => {
231-
console.error("Verify error", err);
232-
throw err;
233-
});
234-
}
235207
if (this._public_key == "") {
236-
throw new Error(this.lang.errs.ERR_NO_KEYS);
208+
throw new Error(this.lang.errs.ERR_NO_KEYS);
237209
}
238210

239211
let alg = {name: "ECDSA", namedCurve: "P-256",
@@ -316,3 +288,89 @@ class VapidToken {
316288
return webCrypto.verify(alg, this._public_key, vsig, t2v);
317289
}
318290
}
291+
292+
class VapidToken01 extends VapidCore {
293+
294+
sign(claims) {
295+
return this._sign(claims)
296+
.then(elements=> {
297+
return {
298+
authorization: "WebPush " + elements.jwt,
299+
"crypto-key": "p256ecdsa=" + elements.pubkey,
300+
publicKey: elements.pubkey,
301+
}
302+
}
303+
)
304+
}
305+
306+
verify(token, public_key) {
307+
let scheme = token.toLowerCase().split(" ")[0]
308+
if (scheme == "bearer" || scheme == "webpush") {
309+
token = token.split(" ")[1];
310+
}
311+
312+
// Again, ideally, just the p256ecdsa token.
313+
if (public_key != null) {
314+
315+
if (public_key.search('p256ecdsa') > -1) {
316+
let sc = /p256ecdsa=([^;,]+)/i;
317+
public_key = sc.exec(public_key)[1];
318+
}
319+
320+
// If there's no public key already defined, load the public_key
321+
// and try again.
322+
return this.import_public_raw(public_key)
323+
.then(key => {
324+
this._public_key = key;
325+
return this._verify(token);
326+
})
327+
.catch(err => {
328+
console.error("Verify error", err);
329+
throw err;
330+
});
331+
}
332+
333+
return this._verify(token)
334+
}
335+
}
336+
337+
class VapidToken02 extends VapidCore {
338+
339+
sign(claims) {
340+
return this._sign(claims)
341+
.then(elements=> {
342+
return {
343+
authorization: "vapid t=" + elements.jwt + ",k=" + elements.pubkey,
344+
publicKey: elements.pubkey,
345+
}
346+
}
347+
)
348+
}
349+
350+
verify(token) {
351+
let scheme = token.toLowerCase().split(" ")[0]
352+
if (scheme == "vapid") {
353+
token = token.split(" ")[1];
354+
}
355+
let vals = {};
356+
let elements = token.split(",");
357+
for (let element of elements) {
358+
let label = element.slice(0,2);
359+
if (label == "t=") {
360+
vals.t = element.slice(2);
361+
}
362+
if (label == "k=") {
363+
vals.k = element.slice(2);
364+
}
365+
}
366+
return this.import_public_raw(vals.k)
367+
.then(key => {
368+
this._public_key = key;
369+
return this._verify(vals.t);
370+
})
371+
.catch(err => {
372+
console.error("Verify error", err);
373+
throw err;
374+
});
375+
}
376+
}

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

0 commit comments

Comments
 (0)