use base64::{self, URL_SAFE_NO_PAD};
use crate::error::WebPushError;
use hyper::Uri;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer as SslSigner;
use serde_json;
use serde_json::{Number, Value};
use std::collections::BTreeMap;
use time;
use crate::vapid::VapidKey;
lazy_static! {
static ref JWT_HEADERS: String = base64::encode_config(
&serde_json::to_string(&json!({
"typ": "JWT",
"alg": "ES256"
}))
.unwrap(),
URL_SAFE_NO_PAD
);
}
#[derive(Debug)]
pub struct VapidSignature {
pub auth_t: String,
pub auth_k: String,
}
impl<'a> Into<String> for &'a VapidSignature {
fn into(self) -> String {
format!("WebPush {}", self.auth_t)
}
}
pub struct VapidSigner {}
impl VapidSigner {
pub fn sign(
key: VapidKey,
endpoint: &Uri,
mut claims: BTreeMap<&str, Value>,
) -> Result<VapidSignature, WebPushError> {
if !claims.contains_key("aud") {
let audience = format!(
"{}://{}",
endpoint.scheme_str().unwrap(),
endpoint.host().unwrap()
);
claims.insert("aud", Value::String(audience));
}
if !claims.contains_key("exp") {
let expiry = time::now_utc() + time::Duration::hours(12);
let number = Number::from(expiry.to_timespec().sec);
claims.insert("exp", Value::Number(number));
}
let signing_input = format!(
"{}.{}",
*JWT_HEADERS,
base64::encode_config(&serde_json::to_string(&claims)?, URL_SAFE_NO_PAD)
);
let public_key = key.public_key();
let auth_k = base64::encode_config(&public_key, URL_SAFE_NO_PAD);
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<u8> = Vec::with_capacity(64);
sigval.extend(r_val);
sigval.extend(s_val);
trace!("Public key: {}", auth_k);
let auth_t = format!(
"{}.{}",
signing_input,
base64::encode_config(&sigval, URL_SAFE_NO_PAD)
);
Ok(VapidSignature { auth_t, auth_k })
}
}
#[cfg(test)]
mod tests {
use crate::vapid::VapidSignature;
#[test]
fn test_vapid_signature_aesgcm_format() {
let vapid_signature = &VapidSignature {
auth_t: String::from("foo"),
auth_k: String::from("bar"),
};
let header_value: String = vapid_signature.into();
assert_eq!("WebPush foo", &header_value);
}
}