diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a09cc09e..f81171b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,5 +57,11 @@ jobs: with: toolchain: ${{matrix.rust}} + - name: Install vcpkg openssl on windows + if: ${{ matrix.os == 'windows-latest' }} + run: | + vcpkg integrate install + vcpkg install openssl:x64-windows-static-md + - name: Run tests - run: cargo test --features=openssl/vendored + run: cargo test diff --git a/Cargo.toml b/Cargo.toml index 4f63a408..aafb1533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "web-push" description = "Web push notification client with support for http-ece encryption and VAPID authentication." -version = "0.8.0" -authors = ["Julius de Bruijn "] +version = "0.9.0" +authors = ["Julius de Bruijn ", "Andrew Ealovega "] license = "Apache-2.0" homepage = "https://github.com/pimeys/rust-web-push" repository = "https://github.com/pimeys/rust-web-push" @@ -12,29 +12,25 @@ keywords = ["web-push", "http-ece", "vapid"] categories = ["web-programming", "asynchronous"] edition = "2018" -[badges] -travis-ci = { repository = "pimeys/rust-web-push" } - [features] -default = ["isahc"] +default = ["isahc", "futures-lite/futures-io"] #futures are only used for read_to_end() in isach client. hyper-client = ["hyper", "hyper-tls"] #use features = ["hyper-client"], default-features = false for about 300kb size decrease. [dependencies] -futures = "^0.3" hyper = { version = "^0.14", features = ["client", "http1"], optional = true } hyper-tls = { version = "^0.5", optional = true } isahc = { version = "^1.4.0", optional = true } +futures-lite = { version = "^1.12", optional = true } http = "^0.2" serde = "^1.0" serde_json = "^1.0" serde_derive = "^1.0" -ring = "^0.16" -ece = "^2.1.0" -native-tls = "^0.2" +jwt-simple = "^0.10.4" +ece = "^2.1" +pem = "^0.8.3" +pkcs8 = { version = "^0.7.5", features = ["alloc"] } +sec1_decode = "^0.1.0" base64 = "^0.13" -openssl = "^0.10" -time = { version = "^0.2", features = ["std"] } -lazy_static = "^1.4" chrono = "^0.4" log = "^0.4" @@ -42,3 +38,4 @@ log = "^0.4" argparse = "^0.2" regex = "^1.5" tokio = { version = "^1.1", features = ["macros", "rt-multi-thread"] } +lazy_static = "^1.4" diff --git a/README.md b/README.md index 4fa2c36f..2069bd9f 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Web push notification sender. ## Requirements -Any async executor for use with client. +Clients require an async executor. System Openssl is needed for compilation. -## Migration to v0.8 +## Migration to greater than v0.7 - The `aesgcm` variant of `ContentEncoding` has been removed. Aes128Gcm support was added in v0.8, so all uses of `ContentEncoding::aesgcm` can simply be changed to `ContentEncoding::Aes128Gcm` with no change to functionality. @@ -26,6 +26,8 @@ Any async executor for use with client. the [fcm crate](https://crates.io/crates/fcm). If you just require web push, you will need to use VAPID to send payloads. See below for info. +- A new error variant `WebPushError::InvalidClaims` has been added. This may break exhaustive matches. + ## Usage To send a web push from command line, first subscribe to receive push notifications with your browser and store the @@ -111,7 +113,7 @@ these claims to the builder manually will override the default values. Overview -------- -Currently, implements +Currently, the crate implements [RFC8188](https://datatracker.ietf.org/doc/html/rfc8188) content encryption for notification payloads. This is done by delegating encryption to mozilla's [ece crate](https://crates.io/crates/ece). Our security is thus tied to [theirs](https://github.com/mozilla/rust-ece/issues/18). The default client is built @@ -120,6 +122,16 @@ on [isahc](https://crates.io/crates/isahc), but can be swapped out with a hyper Library tested with Google's and Mozilla's push notification services. Also verified to work on Edge. +Openssl is needed to build. Install `openssl-dev` or equivalent on *nix, or `openssl` using `vcpkg` on Windows. A nix +script is also available. + +If installing on Windows, this is the exact command: + +```shell +vcpkg integrate install +vcpkg install openssl:x64-windows-static-md +``` + Debugging -------- If you get an error or the push notification doesn't work you can try to debug using the following instructions: diff --git a/src/clients/isahc_client.rs b/src/clients/isahc_client.rs index 08d9dae8..b087d3d5 100644 --- a/src/clients/isahc_client.rs +++ b/src/clients/isahc_client.rs @@ -4,7 +4,7 @@ use isahc::HttpClient; use crate::clients::request_builder; use crate::error::{RetryAfter, WebPushError}; use crate::message::WebPushMessage; -use futures::AsyncReadExt; +use futures_lite::AsyncReadExt; /// An async client for sending the notification payload. This client is expensive to create, and /// should be reused. diff --git a/src/clients/request_builder.rs b/src/clients/request_builder.rs index 73967f36..92caa18f 100644 --- a/src/clients/request_builder.rs +++ b/src/clients/request_builder.rs @@ -96,7 +96,7 @@ mod tests { #[test] fn builds_a_correct_request_with_empty_payload() { //This *was* a real token - let sub = json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x", + let sub = serde_json::json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x", "expirationTime":null, "keys":{"p256dh": "BGa4N1PI79lboMR_YrwCiCsgp35DRvedt7opHcf0yM3iOBTSoQYqQLwWxAfRKE6tsDnReWmhsImkhDF_DBdkNSU", @@ -120,7 +120,7 @@ mod tests { #[test] fn builds_a_correct_request_with_payload() { //This *was* a real token - let sub = json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x", + let sub = serde_json::json!({"endpoint":"https://fcm.googleapis.com/fcm/send/eKClHsXFm9E:APA91bH2x3gNOMv4dF1lQfCgIfOet8EngqKCAUS5DncLOd5hzfSUxcjigIjw9ws-bqa-KmohqiTOcgepAIVO03N39dQfkEkopubML_m3fyvF03pV9_JCB7SxpUjcFmBSVhCaWS6m8l7x", "expirationTime":null, "keys":{"p256dh": "BGa4N1PI79lboMR_YrwCiCsgp35DRvedt7opHcf0yM3iOBTSoQYqQLwWxAfRKE6tsDnReWmhsImkhDF_DBdkNSU", diff --git a/src/error.rs b/src/error.rs index aeb24029..2b2c68be 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,5 @@ use base64::DecodeError; use http::uri::InvalidUri; -use openssl::error::ErrorStack; -use ring::error; use serde_json::error::Error as JsonError; use std::string::FromUtf8Error; use std::time::{Duration, SystemTime}; @@ -44,6 +42,8 @@ pub enum WebPushError { InvalidCryptoKeys, /// Corrupted response data InvalidResponse, + /// A claim had invalid data + InvalidClaims, Other(String), } @@ -67,12 +67,6 @@ impl From for WebPushError { } } -impl From for WebPushError { - fn from(_: error::Unspecified) -> WebPushError { - WebPushError::Unspecified - } -} - #[cfg(feature = "hyper-client")] impl From for WebPushError { fn from(_: hyper::Error) -> Self { @@ -87,24 +81,12 @@ impl From for WebPushError { } } -impl From for WebPushError { - fn from(_: native_tls::Error) -> WebPushError { - WebPushError::TlsError - } -} - impl From for WebPushError { fn from(_: IoError) -> WebPushError { WebPushError::IoError } } -impl From for WebPushError { - fn from(_: ErrorStack) -> WebPushError { - WebPushError::SslError - } -} - impl From for WebPushError { fn from(_: DecodeError) -> WebPushError { WebPushError::InvalidCryptoKeys @@ -132,6 +114,7 @@ impl WebPushError { WebPushError::SslError => "ssl_error", WebPushError::IoError => "io_error", WebPushError::Other(_) => "other", + WebPushError::InvalidClaims => "invalidClaims", } } } @@ -170,6 +153,7 @@ impl fmt::Display for WebPushError { WebPushError::MissingCryptoKeys => write!(f, "The request is missing cryptographic keys"), WebPushError::InvalidCryptoKeys => write!(f, "The request is having invalid cryptographic keys"), WebPushError::Other(_) => write!(f, "An unknown error when connecting the notification service"), + WebPushError::InvalidClaims => write!(f, "At least one JWT claim was invalid.") } } } diff --git a/src/http_ece.rs b/src/http_ece.rs index fac78fe2..ac1bdd7e 100644 --- a/src/http_ece.rs +++ b/src/http_ece.rs @@ -51,6 +51,7 @@ impl<'a> HttpEce<'a> { let mut headers = Vec::new(); + //VAPID uses a special Authorisation header, which contains a ecdhsa key and a jwt. if let Some(signature) = &self.vapid_signature { headers.push(( "Authorization", diff --git a/src/lib.rs b/src/lib.rs index 0fa83fd4..c3903afa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,10 +45,6 @@ #[macro_use] extern crate serde_derive; #[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate serde_json; -#[macro_use] extern crate log; mod clients; diff --git a/src/vapid/builder.rs b/src/vapid/builder.rs index 866aeb74..8b4c9b66 100644 --- a/src/vapid/builder.rs +++ b/src/vapid/builder.rs @@ -1,9 +1,9 @@ use crate::error::WebPushError; use crate::message::SubscriptionInfo; +use crate::vapid::signer::Claims; use crate::vapid::{VapidKey, VapidSignature, VapidSigner}; use http::uri::Uri; -use openssl::ec::EcKey; -use openssl::pkey::Private; +use jwt_simple::prelude::*; use serde_json::Value; use std::collections::BTreeMap; use std::io::Read; @@ -40,7 +40,8 @@ use std::io::Read; /// openssl ec -in private.pem -pubout -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n' /// ``` /// -/// The above commands can be done in code using a library such as openssl if you prefer. +/// The above commands can be done in code using [`PartialVapidSignatureBuilder::get_public_key`], then base64 URL safe +/// encoding as well. /// /// To create a VAPID signature: /// @@ -72,7 +73,7 @@ use std::io::Read; /// ``` pub struct VapidSignatureBuilder<'a> { - claims: BTreeMap<&'a str, Value>, + claims: Claims, key: VapidKey, subscription_info: &'a SubscriptionInfo, } @@ -82,16 +83,13 @@ impl<'a> VapidSignatureBuilder<'a> { /// /// # Details /// - /// This should be the raw private key PEM, including the -----BEGIN EC PRIVATE KEY----- header. - /// If you have a public and private key in the same PEM, the function will still work. + /// The input can be either a pkcs8 formatted PEM, denoted by a -----BEGIN PRIVATE KEY------ + /// header, or a SEC1 formatted PEM, denoted by a -----BEGIN EC PRIVATE KEY------ header. pub fn from_pem( - mut pk_pem: R, + pk_pem: R, subscription_info: &'a SubscriptionInfo, ) -> Result, WebPushError> { - let mut pem_key: Vec = Vec::new(); - pk_pem.read_to_end(&mut pem_key)?; - - let pr_key = EcKey::private_key_from_pem(&pem_key)?; + let pr_key = Self::read_pem(pk_pem)?; Ok(Self::from_ec(pr_key, subscription_info)) } @@ -101,13 +99,10 @@ impl<'a> VapidSignatureBuilder<'a> { /// /// # Details /// - /// This should be the raw private key PEM, including the -----BEGIN EC PRIVATE KEY----- header. - /// If you have a public and private key in the same PEM, the function will still work. - pub fn from_pem_no_sub(mut pk_pem: R) -> Result { - let mut pem_key: Vec = Vec::new(); - pk_pem.read_to_end(&mut pem_key)?; - - let pr_key = EcKey::private_key_from_pem(&pem_key)?; + /// The input can be either a pkcs8 formatted PEM, denoted by a -----BEGIN PRIVATE KEY------ + /// header, or a SEC1 formatted PEM, denoted by a -----BEGIN EC PRIVATE KEY------ header. + pub fn from_pem_no_sub(pk_pem: R) -> Result { + let pr_key = Self::read_pem(pk_pem)?; Ok(PartialVapidSignatureBuilder { key: VapidKey::new(pr_key), @@ -122,7 +117,12 @@ impl<'a> VapidSignatureBuilder<'a> { let mut der_key: Vec = Vec::new(); pk_der.read_to_end(&mut der_key)?; - Ok(Self::from_ec(EcKey::private_key_from_der(&der_key)?, subscription_info)) + let decoded = sec1_decode::parse_der(&der_key).map_err(|_| WebPushError::InvalidCryptoKeys)?; + + Ok(Self::from_ec( + ES256KeyPair::from_bytes(&decoded.key).unwrap(), + subscription_info, + )) } /// Creates a new builder from a DER formatted private key. This function doesn't take a subscription, @@ -131,8 +131,10 @@ impl<'a> VapidSignatureBuilder<'a> { let mut der_key: Vec = Vec::new(); pk_der.read_to_end(&mut der_key)?; + let decoded = sec1_decode::parse_der(&der_key).map_err(|_| WebPushError::InvalidCryptoKeys)?; + Ok(PartialVapidSignatureBuilder { - key: VapidKey::new(EcKey::private_key_from_der(&der_key)?), + key: VapidKey::new(ES256KeyPair::from_bytes(&decoded.key).unwrap()), }) } @@ -146,7 +148,7 @@ impl<'a> VapidSignatureBuilder<'a> { where V: Into, { - self.claims.insert(key, val.into()); + self.claims.custom.insert(key.to_string(), val.into()); } /// Builds a signature to be used in [WebPushMessageBuilder](struct.WebPushMessageBuilder.html). @@ -157,13 +159,37 @@ impl<'a> VapidSignatureBuilder<'a> { Ok(signature) } - fn from_ec(ec_key: EcKey, subscription_info: &'a SubscriptionInfo) -> VapidSignatureBuilder<'a> { + fn from_ec(ec_key: ES256KeyPair, subscription_info: &'a SubscriptionInfo) -> VapidSignatureBuilder<'a> { VapidSignatureBuilder { - claims: BTreeMap::new(), + claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)), key: VapidKey::new(ec_key), subscription_info, } } + + /// Reads the pem file as either format sec1 or pkcs8, then returns the decoded private key. + pub(crate) fn read_pem(mut input: R) -> Result { + let mut buffer = String::new(); + input.read_to_string(&mut buffer).map_err(|_| WebPushError::IoError)?; + //Parse many PEM in the assumption of extra unneeded sections. + let parsed = pem::parse_many(&buffer); + + let found_pkcs8 = parsed.iter().any(|pem| pem.tag == "PRIVATE KEY"); + let found_sec1 = parsed.iter().any(|pem| pem.tag == "EC PRIVATE KEY"); + + //Handle each kind of PEM file differently, as EC keys can be in SEC1 or PKCS8 format. + if found_sec1 { + let key = sec1_decode::parse_pem(buffer.as_bytes()).map_err(|_| WebPushError::InvalidCryptoKeys)?; + Ok(ES256KeyPair::from_bytes(&key.key).map_err(|_| WebPushError::InvalidCryptoKeys)?) + } else if found_pkcs8 { + let key = + pkcs8::PrivateKeyDocument::from_pem(buffer.as_str()).map_err(|_| WebPushError::InvalidCryptoKeys)?; + Ok(ES256KeyPair::from_bytes(key.private_key_info().private_key) + .map_err(|_| WebPushError::InvalidCryptoKeys)?) + } else { + Err(WebPushError::MissingCryptoKeys) + } + } } /// A [`VapidSignatureBuilder`] without VAPID subscription info. @@ -200,12 +226,12 @@ impl<'a> PartialVapidSignatureBuilder { pub fn add_sub_info(self, subscription_info: &'a SubscriptionInfo) -> VapidSignatureBuilder { VapidSignatureBuilder { key: self.key, - claims: BTreeMap::new(), + claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)), subscription_info, } } - /// Gets the public key bytes derived from the private key used for this VAPID signature. + /// Gets the uncompressed public key bytes derived from the private key used for this VAPID signature. /// /// Base64 encode these bytes to get the key to send to the client. pub fn get_public_key(&self) -> Vec { @@ -217,6 +243,7 @@ impl<'a> PartialVapidSignatureBuilder { mod tests { use crate::message::SubscriptionInfo; use crate::vapid::VapidSignatureBuilder; + use ::lazy_static::lazy_static; use std::fs::File; lazy_static! { @@ -227,7 +254,7 @@ mod tests { lazy_static! { static ref SUBSCRIPTION_INFO: SubscriptionInfo = serde_json::from_value( - json!({ + serde_json::json!({ "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABaso4Vajy4STM25r5y5oFfyN451rUmES6mhQngxABxbZB5q_o75WpG25oKdrlrh9KdgWFKdYBc-buLPhvCTqR5KdsK8iCZHQume-ndtZJWKOgJbQ20GjbxHmAT1IAv8AIxTwHO-JTQ2Np2hwkKISp2_KUtpnmwFzglLP7vlCd16hTNJ2I", "keys": { "auth": "sBXU5_tIYz-5w7G2B25BEw", diff --git a/src/vapid/key.rs b/src/vapid/key.rs index 778d6f64..54c21b6f 100644 --- a/src/vapid/key.rs +++ b/src/vapid/key.rs @@ -1,45 +1,36 @@ -use openssl::bn::BigNumContext; -use openssl::ec::{EcGroup, EcKey, PointConversionForm}; -use openssl::nid::Nid; -use openssl::pkey::Private; +use jwt_simple::prelude::*; -#[derive(Clone)] -pub struct VapidKey(pub EcKey); +/// The P256 curve key pair used for VAPID ECDHSA. +pub struct VapidKey(pub ES256KeyPair); -lazy_static! { - static ref GROUP: EcGroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).expect("EC Prime256v1 not supported"); +impl Clone for VapidKey { + fn clone(&self) -> Self { + VapidKey(ES256KeyPair::from_bytes(&self.0.to_bytes()).unwrap()) + } } impl VapidKey { - pub fn new(ec_key: EcKey) -> VapidKey { + pub fn new(ec_key: ES256KeyPair) -> VapidKey { VapidKey(ec_key) } - /// Gets the public key bytes derived from this private key. + /// Gets the uncompressed public key bytes derived from this private key. pub fn public_key(&self) -> Vec { - let mut ctx = BigNumContext::new().unwrap(); - let key = self.0.public_key(); - - key.to_bytes(&*GROUP, PointConversionForm::UNCOMPRESSED, &mut ctx) - .unwrap() + self.0.public_key().public_key().to_bytes_uncompressed() } } #[cfg(test)] mod tests { use crate::vapid::key::VapidKey; - use openssl::ec::EcKey; use std::fs::File; - use std::io::Read; #[test] + /// Tests that VapidKey derives the correct public key. fn test_public_key_derivation() { - let mut f = File::open("resources/vapid_test_key.pem").unwrap(); - let mut pem_key: Vec = Vec::new(); - f.read_to_end(&mut pem_key).unwrap(); - - let ec = EcKey::private_key_from_pem(&pem_key).unwrap(); - let key = VapidKey::new(ec); + let f = File::open("resources/vapid_test_key.pem").unwrap(); + let key = crate::VapidSignatureBuilder::read_pem(f).unwrap(); + let key = VapidKey::new(key); assert_eq!( vec![ @@ -50,4 +41,16 @@ mod tests { key.public_key() ); } + + #[test] + /// Tests that VapidKey clones properly. + fn test_key_clones() { + let f = File::open("resources/vapid_test_key.pem").unwrap(); + let key = crate::VapidSignatureBuilder::read_pem(f).unwrap(); + let key = VapidKey::new(key); + + let key2 = key.clone(); + + assert_eq!(key.0.to_bytes(), key2.0.to_bytes()) + } } diff --git a/src/vapid/mod.rs b/src/vapid/mod.rs index e216ff85..b451404a 100644 --- a/src/vapid/mod.rs +++ b/src/vapid/mod.rs @@ -1,8 +1,11 @@ +//! Contains tooling for signing with VAPID. + pub mod builder; mod key; mod signer; pub use self::builder::VapidSignatureBuilder; use self::key::VapidKey; +pub use self::signer::Claims; pub use self::signer::VapidSignature; use self::signer::VapidSigner; diff --git a/src/vapid/signer.rs b/src/vapid/signer.rs index 598411ff..8d8a07c4 100644 --- a/src/vapid/signer.rs +++ b/src/vapid/signer.rs @@ -1,18 +1,8 @@ use crate::{error::WebPushError, vapid::VapidKey}; -use base64::{self, URL_SAFE_NO_PAD}; use http::uri::Uri; -use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer as SslSigner}; -use serde_json::{Number, Value}; +use jwt_simple::prelude::*; +use serde_json::Value; use std::collections::BTreeMap; -use time::{self, OffsetDateTime}; - -lazy_static! { - /// This is the header of all JWTs. - static ref JWT_HEADERS: String = base64::encode_config( - &serde_json::to_string(&json!({"typ": "JWT","alg": "ES256"})).unwrap(), - URL_SAFE_NO_PAD - ); -} /// A struct representing a VAPID signature. Should be generated using the /// [VapidSignatureBuilder](struct.VapidSignatureBuilder.html). @@ -24,65 +14,39 @@ pub struct VapidSignature { pub auth_k: Vec, } +/// JWT claims object. Custom claims are implemented as a map. +pub type Claims = JWTClaims>; + pub struct VapidSigner {} impl VapidSigner { /// Create a signature with a given key. Sets the default audience from the /// endpoint host and sets the expiry in twelve hours. Values can be /// overwritten by adding the `aud` and `exp` claims. - pub fn sign( - key: VapidKey, - endpoint: &Uri, - mut claims: BTreeMap<&str, Value>, - ) -> Result { - if !claims.contains_key("aud") { + pub fn sign(key: VapidKey, endpoint: &Uri, mut claims: Claims) -> Result { + if !claims.custom.contains_key("aud") { + //Add audience if not provided. let audience = format!("{}://{}", endpoint.scheme_str().unwrap(), endpoint.host().unwrap()); - claims.insert("aud", Value::String(audience)); + claims = claims.with_audience(audience); + } else { + //Use provided claims if given. This is here to avoid breaking changes. + let aud = claims.custom.get("aud").unwrap().clone(); + claims = claims.with_audience(aud); + claims.custom.remove("aud"); } - if !claims.contains_key("exp") { - let expiry = OffsetDateTime::now_utc() + time::Duration::hours(12); - let number = Number::from(expiry.unix_timestamp()); - claims.insert("exp", Value::Number(number)); + //Override the exp claim if provided in custom. Must then remove from custom to avoid printing + //Twice, as this is just for backwards compatibility. + if claims.custom.contains_key("exp") { + let exp = claims.custom.get("exp").unwrap().clone(); + claims.expires_at = Some(Duration::from_secs(exp.as_u64().ok_or(WebPushError::InvalidClaims)?)); + claims.custom.remove("exp"); } - //Generate first half of JWT - let signing_input = format!( - "{}.{}", - *JWT_HEADERS, - base64::encode_config(&serde_json::to_string(&claims)?, URL_SAFE_NO_PAD) - ); - let auth_k = key.public_key(); - let pkey = PKey::from_ec_key(key.0)?; - - let mut signer = SslSigner::new(MessageDigest::sha256(), &pkey)?; - signer.update(signing_input.as_bytes())?; - - let signature = signer.sign_to_vec()?; - - let r_off: usize = 3; - let r_len = signature[r_off] as usize; - let s_off: usize = r_off + r_len + 2; - let s_len = signature[s_off] as usize; - - let mut r_val = &signature[(r_off + 1)..(r_off + 1 + r_len)]; - let mut s_val = &signature[(s_off + 1)..(s_off + 1 + s_len)]; - - if r_len == 33 && r_val[0] == 0 { - r_val = &r_val[1..]; - } - - if s_len == 33 && s_val[0] == 0 { - s_val = &s_val[1..]; - } - - let mut sigval: Vec = Vec::with_capacity(64); - sigval.extend(r_val); - sigval.extend(s_val); - - let auth_t = format!("{}.{}", signing_input, base64::encode_config(&sigval, URL_SAFE_NO_PAD)); + //Generate JWT signature + let auth_t = key.0.sign(claims).map_err(|_| WebPushError::InvalidClaims)?; Ok(VapidSignature { auth_t, auth_k }) }