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}