web_push/vapid/
signer.rs

1use std::collections::BTreeMap;
2
3use http::uri::Uri;
4use jwt_simple::prelude::*;
5use serde_json::Value;
6
7use crate::{error::WebPushError, vapid::VapidKey};
8
9/// A struct representing a VAPID signature. Should be generated using the
10/// [VapidSignatureBuilder](struct.VapidSignatureBuilder.html).
11#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
12pub struct VapidSignature {
13    /// The signed JWT, base64 encoded
14    pub auth_t: String,
15    /// The public key bytes
16    pub auth_k: Vec<u8>,
17}
18
19/// JWT claims object. Custom claims are implemented as a map.
20pub type Claims = JWTClaims<BTreeMap<String /*Use String as lifetimes bug out when serializing a tuple*/, Value>>;
21
22pub struct VapidSigner {}
23
24impl VapidSigner {
25    /// Create a signature with a given key. Sets the default audience from the
26    /// endpoint host and sets the expiry in twelve hours. Values can be
27    /// overwritten by adding the `aud` and `exp` claims.
28    pub fn sign(key: VapidKey, endpoint: &Uri, mut claims: Claims) -> Result<VapidSignature, WebPushError> {
29        if !claims.custom.contains_key("aud") {
30            //Add audience if not provided.
31            let audience = format!("{}://{}", endpoint.scheme_str().unwrap(), endpoint.host().unwrap());
32            claims = claims.with_audience(audience);
33        } else {
34            //Use provided claims if given. This is here to avoid breaking changes.
35            let aud = claims.custom.get("aud").unwrap().clone();
36            //NOTE: This as_str is needed, else \" gets added around the string
37            claims = claims.with_audience(aud.as_str().ok_or(WebPushError::InvalidClaims)?);
38            claims.custom.remove("aud");
39        }
40
41        //Override the exp claim if provided in custom. Must then remove from custom to avoid printing
42        //Twice, as this is just for backwards compatibility.
43        if claims.custom.contains_key("exp") {
44            let exp = claims.custom.get("exp").unwrap().clone();
45            claims.expires_at = Some(Duration::from_secs(exp.as_u64().ok_or(WebPushError::InvalidClaims)?));
46            claims.custom.remove("exp");
47        }
48
49        // Add sub if not provided as some browsers (like firefox) require it even though the API doesn't say its needed >:[
50        if !claims.custom.contains_key("sub") {
51            claims = claims.with_subject("mailto:example@example.com".to_string());
52        }
53
54        log::trace!("Using jwt: {:?}", claims);
55
56        let auth_k = key.public_key();
57
58        //Generate JWT signature
59        let auth_t = key.0.sign(claims).map_err(|_| WebPushError::InvalidClaims)?;
60
61        Ok(VapidSignature { auth_t, auth_k })
62    }
63}
64
65#[cfg(test)]
66mod tests {}