diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..6d7ba37 --- /dev/null +++ b/README.rst @@ -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 `__ +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=…” diff --git a/python/README.md b/python/README.md index 2bb3db5..7877e4d 100644 --- a/python/README.md +++ b/python/README.md @@ -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 @@ -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 @@ -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 diff --git a/python/README.rst b/python/README.rst index e54c498..a2513a8 100644 --- a/python/README.rst +++ b/python/README.rst @@ -1,3 +1,4 @@ +|PyPI version py_vapid| Easy VAPID generation ===================== @@ -95,5 +96,12 @@ Uint8Array `__ +history for details. + .. |PyPI version py_vapid| image:: https://badge.fury.io/py/py-vapid.svg :target: https://pypi.org/project/py-vapid/ diff --git a/python/py_vapid/__init__.py b/python/py_vapid/__init__.py index 76edc62..4a6eff2 100644 --- a/python/py_vapid/__init__.py +++ b/python/py_vapid/__init__.py @@ -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, @@ -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") @@ -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 diff --git a/python/py_vapid/tests/test_vapid.py b/python/py_vapid/tests/test_vapid.py index ec11218..ae9f4e9 100644 --- a/python/py_vapid/tests/test_vapid.py +++ b/python/py_vapid/tests/test_vapid.py @@ -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( @@ -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") diff --git a/rust/vapid/Cargo.toml b/rust/vapid/Cargo.toml index 6dabaff..849d85e 100644 --- a/rust/vapid/Cargo.toml +++ b/rust/vapid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vapid" -version = "0.5.0" +version = "0.6.0" authors = ["jrconlin "] edition = "2021" description = "An implementation of the RFC 8292 Voluntary Application Server Identification (VAPID) Auth header generator" @@ -8,8 +8,9 @@ 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" diff --git a/rust/vapid/src/error.rs b/rust/vapid/src/error.rs index 3a3cefe..08c94a8 100644 --- a/rust/vapid/src/error.rs +++ b/rust/vapid/src/error.rs @@ -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 = result::Result; +pub type VapidResult = result::Result; #[derive(Debug)] pub struct VapidError { - inner: Context, + 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 for VapidError { - fn from(kind: VapidErrorKind) -> VapidError { - Context::new(kind).into() +impl From for VapidError +where + VapidErrorKind: From, +{ + fn from(item: T) -> Self { + VapidError { + kind: VapidErrorKind::from(item), + backtrace: Backtrace::new(), + } } } -impl From> for VapidError { - fn from(inner: Context) -> VapidError { - VapidError { inner } +impl Error for VapidError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.kind.source() } } -impl From 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) } } diff --git a/rust/vapid/src/lib.rs b/rust/vapid/src/lib.rs index 179cdb6..36b904b 100644 --- a/rust/vapid/src/lib.rs +++ b/rust/vapid/src/lib.rs @@ -45,20 +45,24 @@ use openssl::pkey::{PKey, Private, Public}; use openssl::sign::{Signer, Verifier}; mod error; -pub struct Key { - key: EcKey, -} /// 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, +} + 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

(path: P) -> error::VapidResult where P: AsRef, @@ -119,9 +123,12 @@ impl Key { } } +/// The elements of the Authentication. #[derive(Debug)] struct AuthElements { + /// the unjoined JWT components t: Vec, + /// the public verification key k: String, } @@ -205,10 +212,9 @@ pub fn sign( 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( @@ -288,8 +294,8 @@ pub fn sign( )) } +/// Verify that the auth token string matches for the verification token string pub fn verify(auth_token: String) -> Result, 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");