Skip to content

Commit 821b7b8

Browse files
authored
Chore/22 jul update (#100)
* chore: Jul 2022 update Now that this is one of the Critical Projects (thanks?) I should update it to be more modern, clean up some of the crap, and run it through flake8 at least. * Add a rst version of the README * update rust version to 0.6.0
1 parent 973a9cd commit 821b7b8

File tree

8 files changed

+151
-46
lines changed

8 files changed

+151
-46
lines changed

README.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Easy VAPID generation
2+
=====================
3+
4+
A set of VAPID encoding libraries for popular languages.
5+
6+
**PLEASE FEEL FREE TO SUBMIT YOUR FAVORITE LANGUAGE!**
7+
8+
VAPID is a draft specification for providing self identification. see
9+
https://datatracker.ietf.org/doc/draft-ietf-webpush-vapid/ for the
10+
latest specification.
11+
12+
TL;DR:
13+
------
14+
15+
In short, you create a JSON blob that contains some contact information
16+
about your WebPush feed, for instance:
17+
18+
::
19+
20+
{
21+
"aud": "https://YourSiteHere.example",
22+
"sub": "mailto://admin@YourSiteHere.example",
23+
"exp": 1457718878
24+
}
25+
26+
You then convert that to a `JWT <https://tools.ietf.org/html/rfc7519>`__
27+
encoded with\ ``alg = "ES256"``. The resulting token is the
28+
``Authorization`` header “Bearer …” token, the Public Key used to sign
29+
the JWT is added to the ``Crypto-Key`` set as “p256ecdsa=…”

python/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# Easy VAPID generation
2+
13
[![PyPI version py_vapid](https://badge.fury.io/py/py-vapid.svg)](https://pypi.org/project/py-vapid/)
24

3-
# Easy VAPID generation
5+
This library is available on [pypi as py-vapid](https://pypi.python.org/pypi/py-vapid).
6+
Source is available on [github](https://github.com/mozilla-services/vapid).
7+
Please note: This library was designated as a `Critical Project` by PyPi, it is currently
8+
maintained by [a single person](https://xkcd.com/2347/). I still accept PRs and Issues, but
9+
make of that what you will.
410

511
This minimal library contains the minimal set of functions you need to
612
generate a VAPID key set and get the headers you'll need to sign a
@@ -15,9 +21,11 @@ required fields, one semi-optional and several optional additional
1521
fields.
1622

1723
At a minimum a VAPID claim set should look like:
18-
```
24+
25+
```json
1926
{"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServer","exp":"ExpirationTimestamp"}
2027
```
28+
2129
A few notes:
2230

2331
***sub*** is the email address you wish to have on record for this
@@ -56,11 +64,13 @@ app, `bin/vapid`.
5664
You'll need `python virtualenv` Run that in the current directory.
5765

5866
Then run
59-
```
67+
68+
```python
6069
bin/pip install -r requirements.txt
6170

6271
bin/python setup.py install
6372
```
73+
6474
## App Usage
6575

6676
Run by itself, `bin/vapid` will check and optionally create the

python/README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
|PyPI version py_vapid|
12

23
Easy VAPID generation
34
=====================
@@ -95,5 +96,12 @@ Uint8Array <https://github.com/GoogleChrome/push-notifications/blob/master/app/s
9596

9697
See ``bin/vapid -h`` for all options and commands.
9798

99+
CHANGELOG
100+
---------
101+
102+
I’m terrible about updating the Changelog. Please see the
103+
```git log`` <https://github.com/web-push-libs/vapid/pulls?q=is%3Apr+is%3Aclosed>`__
104+
history for details.
105+
98106
.. |PyPI version py_vapid| image:: https://badge.fury.io/py/py-vapid.svg
99107
:target: https://pypi.org/project/py-vapid/

python/py_vapid/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,17 @@ class Vapid02(Vapid01):
316316
_schema = "vapid"
317317

318318
def sign(self, claims, crypto_key=None):
319+
"""Generate an authorization token
320+
321+
:param claims: JSON object containing the JWT claims to use.
322+
:type claims: dict
323+
:param crypto_key: Optional existing crypto_key header content. The
324+
vapid public key will be appended to this data.
325+
:type crypto_key: str
326+
:returns: a hash containing the header fields to use in
327+
the subscription update.
328+
:rtype: dict
329+
"""
319330
sig = sign(self._base_sign(claims), self.private_key)
320331
pkey = self.public_key.public_bytes(
321332
serialization.Encoding.X962,
@@ -331,6 +342,13 @@ def sign(self, claims, crypto_key=None):
331342

332343
@classmethod
333344
def verify(cls, auth):
345+
"""Ensure that the token is correctly formatted and valid
346+
347+
:param auth: An Authorization header
348+
:type auth: str
349+
:rtype: bool
350+
351+
"""
334352
pref_tok = auth.rsplit(' ', 1)
335353
assert pref_tok[0].lower() == cls._schema, (
336354
"Incorrect schema specified")
@@ -349,9 +367,23 @@ def verify(cls, auth):
349367
verification_token=tokens[1]
350368
)
351369

370+
352371
def _check_sub(sub):
353-
pattern =(
354-
r"^(mailto:.+@((localhost|[%\w-]+(\.[%\w-]+)+|([0-9a-f]{1,4}):+([0-9a-f]{1,4})?)))|https:\/\/(localhost|[\w-]+\.[\w\.-]+|([0-9a-f]{1,4}:+)+([0-9a-f]{1,4})?)$"
372+
""" Check to see if the `sub` is a properly formatted `mailto:`
373+
374+
a `mailto:` should be a SMTP mail address. Mind you, since I run
375+
YouFailAtEmail.com, you have every right to yell about how terrible
376+
this check is. I really should be doing a proper component parse
377+
and valiate each component individually per RFC5341, instead I do
378+
the unholy regex you see below.
379+
380+
:param sub: Candidate JWT `sub`
381+
:type sub: str
382+
:rtype: bool
383+
384+
"""
385+
pattern = (
386+
r"^(mailto:.+@((localhost|[%\w-]+(\.[%\w-]+)+|([0-9a-f]{1,4}):+([0-9a-f]{1,4})?)))|https:\/\/(localhost|[\w-]+\.[\w\.-]+|([0-9a-f]{1,4}:+)+([0-9a-f]{1,4})?)$" # noqa
355387
)
356388
return re.match(pattern, sub, re.IGNORECASE) is not None
357389

python/py_vapid/tests/test_vapid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ def test_sign_01(self):
146146
for k in claims:
147147
assert items[k] == claims[k]
148148
result = v.sign(claims)
149-
assert result['Crypto-Key'] == ('p256ecdsa=' +
150-
TEST_KEY_PUBLIC_RAW.decode('utf8'))
149+
assert result['Crypto-Key'] == (
150+
'p256ecdsa=' + TEST_KEY_PUBLIC_RAW.decode('utf8'))
151151
# Verify using the same function as Integration
152152
# this should ensure that the r,s sign values are correctly formed
153153
assert Vapid01.verify(
@@ -210,7 +210,7 @@ def test_bad_integration(self):
210210
"4cCI6MTQ5NDY3MTQ3MCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlb"
211211
"W9AZ2F1bnRmYWNlLmNvLnVrIn0.LqPi86T-HJ71TXHAYFptZEHD7Wlfjcc"
212212
"4u5jYZ17WpqOlqDcW-5Wtx3x1OgYX19alhJ9oLumlS2VzEvNioZ_BAD")
213-
assert Vapid01.verify(key=key, auth=auth) == False
213+
assert not Vapid01.verify(key=key, auth=auth)
214214

215215
def test_bad_sign(self):
216216
v = Vapid01.from_file("/tmp/private")

rust/vapid/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
[package]
22
name = "vapid"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
authors = ["jrconlin <jconlin+git@mozilla.com>"]
55
edition = "2021"
66
description = "An implementation of the RFC 8292 Voluntary Application Server Identification (VAPID) Auth header generator"
77
repository = "https://github.com/web-push-libs/vapid"
88
license = "MPL 2.0"
99

1010
[dependencies]
11+
backtrace="0.3"
1112
openssl = "0.10"
1213
serde_json = "1.0"
1314
base64 = "0.13"
1415
time = "0.3"
15-
failure = "0.1"
16+
thiserror = "1.0"

rust/vapid/src/error.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,76 @@
11
// Error handling based on the failure crate
22

3+
use std::error::Error;
34
use std::fmt;
45
use std::result;
56

6-
use failure::{Backtrace, Context, Error, Fail};
7+
use backtrace::Backtrace;
8+
use thiserror::Error;
79

8-
pub type VapidResult<T> = result::Result<T, Error>;
10+
pub type VapidResult<T> = result::Result<T, VapidError>;
911

1012
#[derive(Debug)]
1113
pub struct VapidError {
12-
inner: Context<VapidErrorKind>,
14+
kind: VapidErrorKind,
15+
pub backtrace: Backtrace,
1316
}
1417

15-
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
18+
#[derive(Debug, Error)]
1619
pub enum VapidErrorKind {
17-
#[fail(display = "Invalid public key")]
20+
/// General IO instance. Can be returned for bad files or key data.
21+
#[error("IO error: {:?}", .0)]
22+
File(#[from] std::io::Error),
23+
/// OpenSSL errors. These tend not to be very specific (or helpful).
24+
#[error("OpenSSL error: {:?}", .0)]
25+
OpenSSL(#[from] openssl::error::ErrorStack),
26+
/// JSON parsing error.
27+
#[error("JSON error:{:?}", .0)]
28+
Json(#[from] serde_json::Error),
29+
30+
/// An invalid public key was specified. Is it EC Prime256v1?
31+
#[error("Invalid public key")]
1832
PublicKey,
19-
#[fail(display = "VAPID error: {}", _0)]
33+
/// A vapid error occurred.
34+
#[error("VAPID error: {}", .0)]
2035
Protocol(String),
21-
#[fail(display = "Internal Error {:?}", _0)]
36+
/// A random internal error
37+
#[error("Internal Error {:?}", .0)]
2238
Internal(String),
2339
}
2440

25-
impl Fail for VapidError {
26-
fn cause(&self) -> Option<&dyn Fail> {
27-
self.inner.cause()
28-
}
29-
30-
fn backtrace(&self) -> Option<&Backtrace> {
31-
self.inner.backtrace()
41+
/// VapidErrors are the general error wrapper that we use. These include
42+
/// a public `backtrace` which can be combined with your own because they're
43+
/// stupidly useful.
44+
impl VapidError {
45+
pub fn kind(&self) -> &VapidErrorKind {
46+
&self.kind
3247
}
33-
}
3448

35-
impl fmt::Display for VapidError {
36-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37-
fmt::Display::fmt(&self.inner, f)
49+
pub fn internal(msg: &str) -> Self {
50+
VapidErrorKind::Internal(msg.to_owned()).into()
3851
}
3952
}
4053

41-
impl From<VapidErrorKind> for VapidError {
42-
fn from(kind: VapidErrorKind) -> VapidError {
43-
Context::new(kind).into()
54+
impl<T> From<T> for VapidError
55+
where
56+
VapidErrorKind: From<T>,
57+
{
58+
fn from(item: T) -> Self {
59+
VapidError {
60+
kind: VapidErrorKind::from(item),
61+
backtrace: Backtrace::new(),
62+
}
4463
}
4564
}
4665

47-
impl From<Context<VapidErrorKind>> for VapidError {
48-
fn from(inner: Context<VapidErrorKind>) -> VapidError {
49-
VapidError { inner }
66+
impl Error for VapidError {
67+
fn source(&self) -> Option<&(dyn Error + 'static)> {
68+
self.kind.source()
5069
}
5170
}
5271

53-
impl From<Error> for VapidError {
54-
fn from(err: Error) -> VapidError {
55-
VapidErrorKind::Internal(format!("Error: {:?}", err)).into()
72+
impl fmt::Display for VapidError {
73+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74+
self.kind.fmt(f)
5675
}
5776
}

rust/vapid/src/lib.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,24 @@ use openssl::pkey::{PKey, Private, Public};
4545
use openssl::sign::{Signer, Verifier};
4646

4747
mod error;
48-
pub struct Key {
49-
key: EcKey<Private>,
50-
}
5148

5249
/// a Key is a helper for creating or using a VAPID EC key.
5350
///
5451
/// Vapid Keys are always Prime256v1 EC keys.
5552
///
53+
pub struct Key {
54+
key: EcKey<Private>,
55+
}
56+
5657
impl Key {
58+
/// return the name of the key.
59+
/// It's always going to be this static value (for now).
60+
/// Eventually it might be "Kevin", but let's not dwell on that.
5761
fn name() -> nid::Nid {
5862
nid::Nid::X9_62_PRIME256V1
5963
}
6064

61-
/// Read a VAPID private key stored in `path`
65+
/// Read a VAPID private key in PEM format stored in `path`
6266
pub fn from_pem<P>(path: P) -> error::VapidResult<Key>
6367
where
6468
P: AsRef<Path>,
@@ -119,9 +123,12 @@ impl Key {
119123
}
120124
}
121125

126+
/// The elements of the Authentication.
122127
#[derive(Debug)]
123128
struct AuthElements {
129+
/// the unjoined JWT components
124130
t: Vec<String>,
131+
/// the public verification key
125132
k: String,
126133
}
127134

@@ -205,10 +212,9 @@ pub fn sign<S: BuildHasher>(
205212
Some(exp) => {
206213
let exp_val = exp.as_i64().unwrap();
207214
if (exp_val as u64) < to_secs(today) {
208-
return Err(error::VapidErrorKind::Protocol(
209-
r#""exp" already expired"#.to_owned(),
210-
)
211-
.into());
215+
return Err(
216+
error::VapidErrorKind::Protocol(r#""exp" already expired"#.to_owned()).into(),
217+
);
212218
}
213219
if (exp_val as u64) > to_secs(tomorrow) {
214220
return Err(error::VapidErrorKind::Protocol(
@@ -288,8 +294,8 @@ pub fn sign<S: BuildHasher>(
288294
))
289295
}
290296

297+
/// Verify that the auth token string matches for the verification token string
291298
pub fn verify(auth_token: String) -> Result<HashMap<String, serde_json::Value>, String> {
292-
//Verify that the auth token string matches for the verification token string
293299
let auth_token = parse_auth_token(&auth_token).expect("Authorization header is invalid.");
294300
let pub_ec_key =
295301
Key::from_public_raw(auth_token.k).expect("'k' token is not a valid public key");

0 commit comments

Comments
 (0)