Skip to main content

web_push_native/
vapid.rs

1//! Utilities for creating VAPID signatures according to [RFC8292](https://datatracker.ietf.org/doc/html/rfc8292).
2//!
3//! [`WebPushBuilder::with_vapid`] can be used to automatically add
4//! VAPID signature headers to your responses. Use [`VapidSignature`]
5//! when you need to manually create and store VAPID signatures.
6//!
7//! # Example
8//!
9//! ```
10//! use base64ct::{Base64UrlUnpadded, Encoding as _};
11//! use web_push_native::{jwt_simple::algorithms::ES256KeyPair, vapid::VapidSignature};
12//!
13//! const VAPID_PRIVATE: &str = "RS0WdYWWo1HajXg3NZR1olzCf31i-ZBGDkFyCs7j1jw";
14//!
15//! let key_pair = ES256KeyPair::from_bytes(&Base64UrlUnpadded::decode_vec(VAPID_PRIVATE)?)?;
16//! let signature = VapidSignature::sign(
17//!     &http::Uri::from_static("https://example.com/"),
18//!     std::time::Duration::new(60, 0),
19//!     "mailto:john.doe@example.com",
20//!     &key_pair,
21//! )?;
22//!
23//! assert!(signature.to_string().starts_with("vapid"));
24//! #
25//! # Ok::<_, Box<dyn std::error::Error>>(())
26//! ```
27
28use std::{fmt::Display, time::Duration};
29
30use base64ct::{Base64UrlUnpadded, Encoding};
31use http::{header, Uri};
32use jwt_simple::{
33    algorithms::{ECDSAP256KeyPairLike, ECDSAP256PublicKeyLike, ES256KeyPair, ES256PublicKey},
34    claims::Claims,
35};
36
37use super::{AddHeaders, WebPushBuilder};
38
39/// Marker for adding VAPID authorization to a [`WebPushBuilder`]
40#[derive(Clone, Debug)]
41pub struct VapidAuthorization<'a> {
42    vapid_kp: &'a ES256KeyPair,
43    contact: &'a str,
44}
45
46impl<'a> VapidAuthorization<'a> {
47    pub(crate) fn new(vapid_kp: &'a ES256KeyPair, contact: &'a str) -> Self {
48        Self { vapid_kp, contact }
49    }
50}
51
52impl<'a> AddHeaders for VapidAuthorization<'a> {
53    type Error = jwt_simple::Error;
54
55    fn add_headers(
56        this: &WebPushBuilder<Self>,
57        builder: http::request::Builder,
58    ) -> Result<http::request::Builder, Self::Error> {
59        let vapid = VapidSignature::sign(
60            &this.endpoint,
61            this.valid_duration,
62            this.http_auth.contact.to_string(),
63            this.http_auth.vapid_kp,
64        )?;
65        Ok(builder.header(header::AUTHORIZATION, vapid))
66    }
67}
68
69/// VAPID (Voluntary Application Server Identification) signature
70#[derive(Clone, Debug)]
71pub struct VapidSignature {
72    token: String,
73    public_key: ES256PublicKey,
74}
75
76impl VapidSignature {
77    /// Creates and signs a new [`VapidSignature`] which can be used
78    /// as a HTTP header value.
79    pub fn sign<T: ToString>(
80        endpoint: &Uri,
81        valid_duration: Duration,
82        contact: T,
83        key: &ES256KeyPair,
84    ) -> Result<VapidSignature, jwt_simple::Error> {
85        let claims = Claims::create(valid_duration.into())
86            .with_audience(format!(
87                "{}://{}",
88                endpoint
89                    .scheme_str()
90                    .ok_or(jwt_simple::Error::msg("missing scheme in endpoint"))?,
91                endpoint
92                    .host()
93                    .ok_or(jwt_simple::Error::msg("missing host in endpoint"))?
94            ))
95            .with_subject(contact);
96
97        Ok(VapidSignature {
98            token: key.sign(claims)?,
99            public_key: key.public_key(),
100        })
101    }
102}
103
104impl Display for VapidSignature {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        let encoded_public =
107            Base64UrlUnpadded::encode_string(&self.public_key.public_key().to_bytes_uncompressed());
108        write!(f, "vapid t={}, k={}", self.token, encoded_public)
109    }
110}
111
112impl From<VapidSignature> for http::HeaderValue {
113    fn from(signature: VapidSignature) -> Self {
114        Self::try_from(signature.to_string()).expect("given string is always a valid header value")
115    }
116}