web_push/vapid/
builder.rs

1use std::collections::BTreeMap;
2use std::io::Read;
3
4use http::uri::Uri;
5use jwt_simple::prelude::*;
6use serde_json::Value;
7
8use crate::error::WebPushError;
9use crate::message::SubscriptionInfo;
10use crate::vapid::signer::Claims;
11use crate::vapid::{VapidKey, VapidSignature, VapidSigner};
12
13/// A VAPID signature builder for generating an optional signature to the
14/// request. This encryption is required for payloads in all current and future browsers.
15///
16/// To communicate with the site, one needs to generate a private key to keep in
17/// the server and derive a public key from the generated private key for the
18/// client.
19///
20/// Private key generation:
21///
22/// ```bash,ignore
23/// openssl ecparam -name prime256v1 -genkey -noout -out private.pem
24/// ```
25///
26/// To derive a public key out of generated private key:
27///
28/// ```bash,ignore
29/// openssl ec -in private.pem -pubout -out vapid_public.pem
30/// ```
31///
32/// To get the byte form of the public key for the JavaScript client:
33///
34/// ```bash,ignore
35/// openssl ec -in private.pem -text -noout -conv_form uncompressed
36/// ```
37///
38/// ... or a base64 encoded string, which the client should convert into
39/// byte form before using:
40///
41/// ```bash,ignore
42/// openssl ec -in private.pem -pubout -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'
43/// ```
44///
45/// The above commands can be done in code using [`PartialVapidSignatureBuilder::get_public_key`], then base64 URL safe
46/// encoding as well.
47///
48/// To create a VAPID signature:
49///
50/// ```no_run
51/// # extern crate web_push;
52/// # use web_push::*;
53/// # use std::fs::File;
54/// # fn main () {
55/// //You would get this as a `pushSubscription` object from the client. They need your public key to get that object.
56/// let subscription_info = SubscriptionInfo {
57///     keys: SubscriptionKeys {
58///         p256dh: String::from("something"),
59///         auth: String::from("secret"),
60///     },
61///     endpoint: String::from("https://mozilla.rules/something"),
62/// };
63///
64/// let file = File::open("private.pem").unwrap();
65///
66/// let mut sig_builder = VapidSignatureBuilder::from_pem(file, &subscription_info).unwrap();
67///
68/// //These fields are optional, and likely unneeded for most uses.
69/// sig_builder.add_claim("sub", "mailto:test@example.com");
70/// sig_builder.add_claim("foo", "bar");
71/// sig_builder.add_claim("omg", 123);
72///
73/// let signature = sig_builder.build().unwrap();
74/// # }
75/// ```
76pub struct VapidSignatureBuilder<'a> {
77    claims: Claims,
78    key: VapidKey,
79    subscription_info: &'a SubscriptionInfo,
80}
81
82impl<'a> VapidSignatureBuilder<'a> {
83    /// Creates a new builder from a PEM formatted private key.
84    ///
85    /// # Details
86    ///
87    /// The input can be either a pkcs8 formatted PEM, denoted by a -----BEGIN PRIVATE KEY------
88    /// header, or a SEC1 formatted PEM, denoted by a -----BEGIN EC PRIVATE KEY------ header.
89    pub fn from_pem<R: Read>(
90        pk_pem: R,
91        subscription_info: &'a SubscriptionInfo,
92    ) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
93        let pr_key = Self::read_pem(pk_pem)?;
94
95        Ok(Self::from_ec(pr_key, subscription_info))
96    }
97
98    /// Creates a new builder from a PEM formatted private key. This function doesn't take a subscription,
99    /// allowing the reuse of one builder for multiple messages by cloning the resulting builder.
100    ///
101    /// # Details
102    ///
103    /// The input can be either a pkcs8 formatted PEM, denoted by a -----BEGIN PRIVATE KEY------
104    /// header, or a SEC1 formatted PEM, denoted by a -----BEGIN EC PRIVATE KEY------ header.
105    pub fn from_pem_no_sub<R: Read>(pk_pem: R) -> Result<PartialVapidSignatureBuilder, WebPushError> {
106        let pr_key = Self::read_pem(pk_pem)?;
107
108        Ok(PartialVapidSignatureBuilder {
109            key: VapidKey::new(pr_key),
110        })
111    }
112
113    /// Creates a new builder from a DER formatted private key.
114    pub fn from_der<R: Read>(
115        mut pk_der: R,
116        subscription_info: &'a SubscriptionInfo,
117    ) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
118        let mut der_key: Vec<u8> = Vec::new();
119        pk_der.read_to_end(&mut der_key)?;
120
121        Ok(Self::from_ec(
122            ES256KeyPair::from_bytes(
123                &sec1_decode::parse_der(&der_key)
124                    .map_err(|_| WebPushError::InvalidCryptoKeys)?
125                    .key,
126            )
127            .map_err(|_| WebPushError::InvalidCryptoKeys)?,
128            subscription_info,
129        ))
130    }
131
132    /// Creates a new builder from a DER formatted private key. This function doesn't take a subscription,
133    /// allowing the reuse of one builder for multiple messages by cloning the resulting builder.
134    pub fn from_der_no_sub<R: Read>(mut pk_der: R) -> Result<PartialVapidSignatureBuilder, WebPushError> {
135        let mut der_key: Vec<u8> = Vec::new();
136        pk_der.read_to_end(&mut der_key)?;
137
138        Ok(PartialVapidSignatureBuilder {
139            key: VapidKey::new(
140                ES256KeyPair::from_bytes(
141                    &sec1_decode::parse_der(&der_key)
142                        .map_err(|_| WebPushError::InvalidCryptoKeys)?
143                        .key,
144                )
145                .map_err(|_| WebPushError::InvalidCryptoKeys)?,
146            ),
147        })
148    }
149
150    /// Creates a new builder from a raw base64 encoded private key. This isn't the base64 from a key
151    /// generated by openssl, but rather the literal bytes of the private key itself. This is the kind
152    /// of key given to you by most VAPID key generator sites, and also the kind used in the API of other
153    /// large web push libraries, such as PHP and Node.
154    ///
155    /// # Config
156    /// base64 has multiple encodings itself, the most common of which for web push is URL_SAFE_NO_PAD.
157    /// This function does support other encodings however, if needed.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// # use web_push::VapidSignatureBuilder;
163    /// // Use `from_base64` here if you have a sub
164    /// let builder = VapidSignatureBuilder::from_base64_no_sub("IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY", base64::URL_SAFE_NO_PAD).unwrap();
165    /// ```
166    pub fn from_base64(
167        encoded: &str,
168        config: base64::Config,
169        subscription_info: &'a SubscriptionInfo,
170    ) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
171        let pr_key = ES256KeyPair::from_bytes(
172            &base64::decode_config(encoded, config).map_err(|_| WebPushError::InvalidCryptoKeys)?,
173        )
174        .map_err(|_| WebPushError::InvalidCryptoKeys)?;
175
176        Ok(Self::from_ec(pr_key, subscription_info))
177    }
178
179    /// Creates a new builder from a raw base64 encoded private key. This function doesn't take a subscription,
180    /// allowing the reuse of one builder for multiple messages by cloning the resulting builder.
181    pub fn from_base64_no_sub(
182        encoded: &str,
183        config: base64::Config,
184    ) -> Result<PartialVapidSignatureBuilder, WebPushError> {
185        let pr_key = ES256KeyPair::from_bytes(
186            &base64::decode_config(encoded, config).map_err(|_| WebPushError::InvalidCryptoKeys)?,
187        )
188        .map_err(|_| WebPushError::InvalidCryptoKeys)?;
189
190        Ok(PartialVapidSignatureBuilder {
191            key: VapidKey::new(pr_key),
192        })
193    }
194
195    /// Add a claim to the signature. Claims `aud` and `exp` are automatically
196    /// added to the signature. Add them manually to override the default
197    /// values.
198    ///
199    /// The function accepts any value that can be converted into a type JSON
200    /// supports.
201    pub fn add_claim<V>(&mut self, key: &'a str, val: V)
202    where
203        V: Into<Value>,
204    {
205        self.claims.custom.insert(key.to_string(), val.into());
206    }
207
208    /// Builds a signature to be used in [WebPushMessageBuilder](struct.WebPushMessageBuilder.html).
209    pub fn build(self) -> Result<VapidSignature, WebPushError> {
210        let endpoint: Uri = self.subscription_info.endpoint.parse()?;
211        let signature = VapidSigner::sign(self.key, &endpoint, self.claims)?;
212
213        Ok(signature)
214    }
215
216    fn from_ec(ec_key: ES256KeyPair, subscription_info: &'a SubscriptionInfo) -> VapidSignatureBuilder<'a> {
217        VapidSignatureBuilder {
218            claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)),
219            key: VapidKey::new(ec_key),
220            subscription_info,
221        }
222    }
223
224    /// Reads the pem file as either format sec1 or pkcs8, then returns the decoded private key.
225    pub(crate) fn read_pem<R: Read>(mut input: R) -> Result<ES256KeyPair, WebPushError> {
226        let mut buffer = String::new();
227        input.read_to_string(&mut buffer).map_err(|_| WebPushError::IoError)?;
228
229        //Parse many PEM in the assumption of extra unneeded sections.
230        let parsed = pem::parse_many(&buffer).map_err(|_| WebPushError::InvalidCryptoKeys)?;
231
232        let found_pkcs8 = parsed.iter().any(|pem| pem.tag() == "PRIVATE KEY");
233        let found_sec1 = parsed.iter().any(|pem| pem.tag() == "EC PRIVATE KEY");
234
235        //Handle each kind of PEM file differently, as EC keys can be in SEC1 or PKCS8 format.
236        if found_sec1 {
237            let key = sec1_decode::parse_pem(buffer.as_bytes()).map_err(|_| WebPushError::InvalidCryptoKeys)?;
238            Ok(ES256KeyPair::from_bytes(&key.key).map_err(|_| WebPushError::InvalidCryptoKeys)?)
239        } else if found_pkcs8 {
240            Ok(ES256KeyPair::from_pem(&buffer).map_err(|_| WebPushError::InvalidCryptoKeys)?)
241        } else {
242            Err(WebPushError::MissingCryptoKeys)
243        }
244    }
245}
246
247/// A [`VapidSignatureBuilder`] without VAPID subscription info.
248///
249/// # Example
250///
251/// ```no_run
252/// use web_push::{VapidSignatureBuilder, SubscriptionInfo};
253///
254/// let builder = VapidSignatureBuilder::from_pem_no_sub("Some PEM".as_bytes()).unwrap();
255///
256/// //Clone builder for each use of the same private key
257/// {
258///     //Pretend this changes for each connection
259///     let subscription_info = SubscriptionInfo::new(
260///     "https://updates.push.services.mozilla.com/wpush/v1/...",
261///     "BLMbF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
262///     "xS03Fi5ErfTNH_l9WHE9Ig"
263///     );
264///
265///     let builder = builder.clone();
266///     let sig = builder.add_sub_info(&subscription_info).build();
267///     //Sign message ect.
268/// }
269///
270/// ```
271#[derive(Clone)]
272pub struct PartialVapidSignatureBuilder {
273    key: VapidKey,
274}
275
276impl PartialVapidSignatureBuilder {
277    /// Adds the VAPID subscription info for a particular client.
278    pub fn add_sub_info(self, subscription_info: &SubscriptionInfo) -> VapidSignatureBuilder<'_> {
279        VapidSignatureBuilder {
280            key: self.key,
281            claims: jwt_simple::prelude::Claims::with_custom_claims(BTreeMap::new(), Duration::from_hours(12)),
282            subscription_info,
283        }
284    }
285
286    /// Gets the uncompressed public key bytes derived from the private key used for this VAPID signature.
287    ///
288    /// Base64 encode these bytes to get the key to send to the client.
289    pub fn get_public_key(&self) -> Vec<u8> {
290        self.key.public_key()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use std::fs::File;
297
298    use ::lazy_static::lazy_static;
299
300    use crate::message::SubscriptionInfo;
301    use crate::vapid::VapidSignatureBuilder;
302
303    lazy_static! {
304        static ref PRIVATE_PEM: File = File::open("resources/vapid_test_key.pem").unwrap();
305        static ref PRIVATE_DER: File = File::open("resources/vapid_test_key.der").unwrap();
306    }
307
308    lazy_static! {
309        static ref SUBSCRIPTION_INFO: SubscriptionInfo =
310            serde_json::from_value(
311                serde_json::json!({
312                    "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABaso4Vajy4STM25r5y5oFfyN451rUmES6mhQngxABxbZB5q_o75WpG25oKdrlrh9KdgWFKdYBc-buLPhvCTqR5KdsK8iCZHQume-ndtZJWKOgJbQ20GjbxHmAT1IAv8AIxTwHO-JTQ2Np2hwkKISp2_KUtpnmwFzglLP7vlCd16hTNJ2I",
313                    "keys": {
314                        "auth": "sBXU5_tIYz-5w7G2B25BEw",
315                        "p256dh": "BH1HTeKM7-NwaLGHEqxeu2IamQaVVLkcsFHPIHmsCnqxcBHPQBprF41bEMOr3O1hUQ2jU1opNEm1F_lZV_sxMP8"
316                    }
317                })
318            ).unwrap();
319    }
320
321    static PRIVATE_BASE64: &str = "IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY";
322
323    #[test]
324    fn test_builder_from_pem() {
325        let builder = VapidSignatureBuilder::from_pem(&*PRIVATE_PEM, &SUBSCRIPTION_INFO).unwrap();
326        let signature = builder.build().unwrap();
327
328        assert_eq!(
329            "BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
330            base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
331        );
332
333        assert!(!signature.auth_t.is_empty());
334    }
335
336    #[test]
337    fn test_builder_from_der() {
338        let builder = VapidSignatureBuilder::from_der(&*PRIVATE_DER, &SUBSCRIPTION_INFO).unwrap();
339        let signature = builder.build().unwrap();
340
341        assert_eq!(
342            "BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
343            base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
344        );
345
346        assert!(!signature.auth_t.is_empty());
347    }
348
349    #[test]
350    fn test_builder_from_base64() {
351        let builder =
352            VapidSignatureBuilder::from_base64(PRIVATE_BASE64, base64::URL_SAFE_NO_PAD, &SUBSCRIPTION_INFO).unwrap();
353        let signature = builder.build().unwrap();
354
355        assert_eq!(
356            "BMjQIp55pdbU8pfCBKyXcZjlmER_mXt5LqNrN1hrXbdBS5EnhIbMu3Au-RV53iIpztzNXkGI56BFB1udQ8Bq_H4",
357            base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
358        );
359
360        assert!(!signature.auth_t.is_empty());
361    }
362}