Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Easy VAPID generation
=====================

A set of VAPID encoding libraries for popular languages.

**PLEASE FEEL FREE TO SUBMIT YOUR FAVORITE LANGUAGE!**

VAPID is a draft specification for providing self identification. see
https://datatracker.ietf.org/doc/draft-ietf-webpush-vapid/ for the
latest specification.

TL;DR:
------

In short, you create a JSON blob that contains some contact information
about your WebPush feed, for instance:

::

{
"aud": "https://YourSiteHere.example",
"sub": "mailto://admin@YourSiteHere.example",
"exp": 1457718878
}

You then convert that to a `JWT <https://tools.ietf.org/html/rfc7519>`__
encoded with\ ``alg = "ES256"``. The resulting token is the
``Authorization`` header “Bearer …” token, the Public Key used to sign
the JWT is added to the ``Crypto-Key`` set as “p256ecdsa=…”
16 changes: 13 additions & 3 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Easy VAPID generation

[![PyPI version py_vapid](https://badge.fury.io/py/py-vapid.svg)](https://pypi.org/project/py-vapid/)

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

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

At a minimum a VAPID claim set should look like:
```

```json
{"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServer","exp":"ExpirationTimestamp"}
```

A few notes:

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

Then run
```

```python
bin/pip install -r requirements.txt

bin/python setup.py install
```

## App Usage

Run by itself, `bin/vapid` will check and optionally create the
Expand Down
8 changes: 8 additions & 0 deletions python/README.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
|PyPI version py_vapid|

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

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

CHANGELOG
---------

I’m terrible about updating the Changelog. Please see the
```git log`` <https://github.com/web-push-libs/vapid/pulls?q=is%3Apr+is%3Aclosed>`__
history for details.

.. |PyPI version py_vapid| image:: https://badge.fury.io/py/py-vapid.svg
:target: https://pypi.org/project/py-vapid/
36 changes: 34 additions & 2 deletions python/py_vapid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,17 @@ class Vapid02(Vapid01):
_schema = "vapid"

def sign(self, claims, crypto_key=None):
"""Generate an authorization token
:param claims: JSON object containing the JWT claims to use.
:type claims: dict
:param crypto_key: Optional existing crypto_key header content. The
vapid public key will be appended to this data.
:type crypto_key: str
:returns: a hash containing the header fields to use in
the subscription update.
:rtype: dict
"""
sig = sign(self._base_sign(claims), self.private_key)
pkey = self.public_key.public_bytes(
serialization.Encoding.X962,
Expand All @@ -331,6 +342,13 @@ def sign(self, claims, crypto_key=None):

@classmethod
def verify(cls, auth):
"""Ensure that the token is correctly formatted and valid
:param auth: An Authorization header
:type auth: str
:rtype: bool
"""
pref_tok = auth.rsplit(' ', 1)
assert pref_tok[0].lower() == cls._schema, (
"Incorrect schema specified")
Expand All @@ -349,9 +367,23 @@ def verify(cls, auth):
verification_token=tokens[1]
)


def _check_sub(sub):
pattern =(
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})?)$"
""" Check to see if the `sub` is a properly formatted `mailto:`
a `mailto:` should be a SMTP mail address. Mind you, since I run
YouFailAtEmail.com, you have every right to yell about how terrible
this check is. I really should be doing a proper component parse
and valiate each component individually per RFC5341, instead I do
the unholy regex you see below.
:param sub: Candidate JWT `sub`
:type sub: str
:rtype: bool
"""
pattern = (
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
)
return re.match(pattern, sub, re.IGNORECASE) is not None

Expand Down
6 changes: 3 additions & 3 deletions python/py_vapid/tests/test_vapid.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ def test_sign_01(self):
for k in claims:
assert items[k] == claims[k]
result = v.sign(claims)
assert result['Crypto-Key'] == ('p256ecdsa=' +
TEST_KEY_PUBLIC_RAW.decode('utf8'))
assert result['Crypto-Key'] == (
'p256ecdsa=' + TEST_KEY_PUBLIC_RAW.decode('utf8'))
# Verify using the same function as Integration
# this should ensure that the r,s sign values are correctly formed
assert Vapid01.verify(
Expand Down Expand Up @@ -210,7 +210,7 @@ def test_bad_integration(self):
"4cCI6MTQ5NDY3MTQ3MCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlb"
"W9AZ2F1bnRmYWNlLmNvLnVrIn0.LqPi86T-HJ71TXHAYFptZEHD7Wlfjcc"
"4u5jYZ17WpqOlqDcW-5Wtx3x1OgYX19alhJ9oLumlS2VzEvNioZ_BAD")
assert Vapid01.verify(key=key, auth=auth) == False
assert not Vapid01.verify(key=key, auth=auth)

def test_bad_sign(self):
v = Vapid01.from_file("/tmp/private")
Expand Down
5 changes: 3 additions & 2 deletions rust/vapid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
[package]
name = "vapid"
version = "0.5.0"
version = "0.6.0"
authors = ["jrconlin <jconlin+git@mozilla.com>"]
edition = "2021"
description = "An implementation of the RFC 8292 Voluntary Application Server Identification (VAPID) Auth header generator"
repository = "https://github.com/web-push-libs/vapid"
license = "MPL 2.0"

[dependencies]
backtrace="0.3"
openssl = "0.10"
serde_json = "1.0"
base64 = "0.13"
time = "0.3"
failure = "0.1"
thiserror = "1.0"
73 changes: 46 additions & 27 deletions rust/vapid/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,76 @@
// Error handling based on the failure crate

use std::error::Error;
use std::fmt;
use std::result;

use failure::{Backtrace, Context, Error, Fail};
use backtrace::Backtrace;
use thiserror::Error;

pub type VapidResult<T> = result::Result<T, Error>;
pub type VapidResult<T> = result::Result<T, VapidError>;

#[derive(Debug)]
pub struct VapidError {
inner: Context<VapidErrorKind>,
kind: VapidErrorKind,
pub backtrace: Backtrace,
}

#[derive(Clone, Eq, PartialEq, Debug, Fail)]
#[derive(Debug, Error)]
pub enum VapidErrorKind {
#[fail(display = "Invalid public key")]
/// General IO instance. Can be returned for bad files or key data.
#[error("IO error: {:?}", .0)]
File(#[from] std::io::Error),
/// OpenSSL errors. These tend not to be very specific (or helpful).
#[error("OpenSSL error: {:?}", .0)]
OpenSSL(#[from] openssl::error::ErrorStack),
/// JSON parsing error.
#[error("JSON error:{:?}", .0)]
Json(#[from] serde_json::Error),

/// An invalid public key was specified. Is it EC Prime256v1?
#[error("Invalid public key")]
PublicKey,
#[fail(display = "VAPID error: {}", _0)]
/// A vapid error occurred.
#[error("VAPID error: {}", .0)]
Protocol(String),
#[fail(display = "Internal Error {:?}", _0)]
/// A random internal error
#[error("Internal Error {:?}", .0)]
Internal(String),
}

impl Fail for VapidError {
fn cause(&self) -> Option<&dyn Fail> {
self.inner.cause()
}

fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
/// VapidErrors are the general error wrapper that we use. These include
/// a public `backtrace` which can be combined with your own because they're
/// stupidly useful.
impl VapidError {
pub fn kind(&self) -> &VapidErrorKind {
&self.kind
}
}

impl fmt::Display for VapidError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.inner, f)
pub fn internal(msg: &str) -> Self {
VapidErrorKind::Internal(msg.to_owned()).into()
}
}

impl From<VapidErrorKind> for VapidError {
fn from(kind: VapidErrorKind) -> VapidError {
Context::new(kind).into()
impl<T> From<T> for VapidError
where
VapidErrorKind: From<T>,
{
fn from(item: T) -> Self {
VapidError {
kind: VapidErrorKind::from(item),
backtrace: Backtrace::new(),
}
}
}

impl From<Context<VapidErrorKind>> for VapidError {
fn from(inner: Context<VapidErrorKind>) -> VapidError {
VapidError { inner }
impl Error for VapidError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.kind.source()
}
}

impl From<Error> for VapidError {
fn from(err: Error) -> VapidError {
VapidErrorKind::Internal(format!("Error: {:?}", err)).into()
impl fmt::Display for VapidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
}
24 changes: 15 additions & 9 deletions rust/vapid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,24 @@ use openssl::pkey::{PKey, Private, Public};
use openssl::sign::{Signer, Verifier};

mod error;
pub struct Key {
key: EcKey<Private>,
}

/// a Key is a helper for creating or using a VAPID EC key.
///
/// Vapid Keys are always Prime256v1 EC keys.
///
pub struct Key {
key: EcKey<Private>,
}

impl Key {
/// return the name of the key.
/// It's always going to be this static value (for now).
/// Eventually it might be "Kevin", but let's not dwell on that.
fn name() -> nid::Nid {
nid::Nid::X9_62_PRIME256V1
}

/// Read a VAPID private key stored in `path`
/// Read a VAPID private key in PEM format stored in `path`
pub fn from_pem<P>(path: P) -> error::VapidResult<Key>
where
P: AsRef<Path>,
Expand Down Expand Up @@ -119,9 +123,12 @@ impl Key {
}
}

/// The elements of the Authentication.
#[derive(Debug)]
struct AuthElements {
/// the unjoined JWT components
t: Vec<String>,
/// the public verification key
k: String,
}

Expand Down Expand Up @@ -205,10 +212,9 @@ pub fn sign<S: BuildHasher>(
Some(exp) => {
let exp_val = exp.as_i64().unwrap();
if (exp_val as u64) < to_secs(today) {
return Err(error::VapidErrorKind::Protocol(
r#""exp" already expired"#.to_owned(),
)
.into());
return Err(
error::VapidErrorKind::Protocol(r#""exp" already expired"#.to_owned()).into(),
);
}
if (exp_val as u64) > to_secs(tomorrow) {
return Err(error::VapidErrorKind::Protocol(
Expand Down Expand Up @@ -288,8 +294,8 @@ pub fn sign<S: BuildHasher>(
))
}

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