From 04fee7f9cbb7fc984d34953c05f34f7a732310d4 Mon Sep 17 00:00:00 2001 From: Mark Drobnak Date: Fri, 26 Jun 2020 09:35:16 -0400 Subject: [PATCH] feat: Validate autoendpoint JWT tokens (#154) * Use an enum for API version and add VapidHeaderWithKey * Perform expiration time validation on the JWT * Perform JWT claims extraction * Use the jsonwebtoken library for JWT validation * Add back VapidError::FutureExpirationToken and associated check The JWT library doesn't handle this error. * Record another metric for vapid version * Simplify handling of VAPID v2 in extract_public_key Closes #103 --- Cargo.lock | 75 ++++++++++++++++ autoendpoint/Cargo.toml | 1 + autoendpoint/src/error.rs | 7 +- .../src/server/extractors/subscription.rs | 85 ++++++++++++++----- .../src/server/extractors/token_info.rs | 36 +++++--- autoendpoint/src/server/headers/vapid.rs | 13 ++- 6 files changed, 179 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fab79372..3157fb85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,7 @@ dependencies = [ "fernet 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "jsonwebtoken 7.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1569,6 +1570,19 @@ dependencies = [ "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "jsonwebtoken" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pem 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1829,6 +1843,16 @@ dependencies = [ "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -1954,6 +1978,16 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pem" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -2351,6 +2385,20 @@ dependencies = [ "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ring" +version = "0.16.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rusoto_core" version = "0.42.0" @@ -2688,6 +2736,16 @@ dependencies = [ "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "simple_asn1" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "sized-chunks" version = "0.5.1" @@ -2808,6 +2866,11 @@ name = "sourcefile" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "state_machine_future" version = "0.2.0" @@ -3399,6 +3462,11 @@ name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "url" version = "1.7.2" @@ -3851,6 +3919,7 @@ dependencies = [ "checksum ipconfig 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" +"checksum jsonwebtoken 7.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c6afc14dc098f780c4ec8ec3d7b2bd8ac5d5c9ed031d55d8e0a25378010ae444" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -3882,6 +3951,7 @@ dependencies = [ "checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" "checksum nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +"checksum num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" @@ -3896,6 +3966,7 @@ dependencies = [ "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" "checksum parking_lot_core 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +"checksum pem 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "59698ea79df9bf77104aefd39cc3ec990cb9693fb59c3b0a70ddf2646fdffb4b" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum petgraph 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" @@ -3939,6 +4010,7 @@ dependencies = [ "checksum reqwest 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680" "checksum reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" "checksum resolv-conf 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a" +"checksum ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c" "checksum rusoto_core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f1d1ecfe8dac29878a713fbc4c36b0a84a48f7a6883541841cdff9fdd2ba7dfb" "checksum rusoto_credential 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8632e41d289db90dd40d0389c71a23c5489e3afd448424226529113102e2a002" "checksum rusoto_dynamodb 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7f6cfb692b9300c14d0d7c2607b2dcb9da8afca4239f3ca4e9ec48f47696ac9d" @@ -3972,6 +4044,7 @@ dependencies = [ "checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" "checksum signal-hook 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6" "checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" +"checksum simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" "checksum sized-chunks 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f59f81ec9833a580d2448e958d16bd872637798f3ab300b693c48f136fb76ff" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum slog 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" @@ -3985,6 +4058,7 @@ dependencies = [ "checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" +"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum state_machine_future 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "530e1d624baae485bce12e6647acb76aafa253346ee8a16751974eed5a24b13d" "checksum static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" @@ -4041,6 +4115,7 @@ dependencies = [ "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" "checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" diff --git a/autoendpoint/Cargo.toml b/autoendpoint/Cargo.toml index 0961b9cb..d3f4491f 100644 --- a/autoendpoint/Cargo.toml +++ b/autoendpoint/Cargo.toml @@ -18,6 +18,7 @@ docopt = "1.1.0" fernet = "0.1.3" futures = { version = "0.3", features = ["compat"] } hex = "0.4.2" +jsonwebtoken = "7.1.1" lazy_static = "1.4.0" openssl = "0.10" regex = "1.3" diff --git a/autoendpoint/src/error.rs b/autoendpoint/src/error.rs index 41e1b75c..b4cc27f6 100644 --- a/autoendpoint/src/error.rs +++ b/autoendpoint/src/error.rs @@ -62,8 +62,11 @@ pub enum ApiErrorKind { #[error(transparent)] Uuid(#[from] uuid::Error), + #[error(transparent)] + Jwt(#[from] jsonwebtoken::errors::Error), + #[error("Error while validating token")] - TokenHashValidation(#[from] openssl::error::ErrorStack), + TokenHashValidation(#[source] openssl::error::ErrorStack), #[error("Database error: {0}")] Database(#[source] autopush_common::errors::Error), @@ -102,7 +105,7 @@ impl ApiErrorKind { ApiErrorKind::NoSubscription => StatusCode::GONE, - ApiErrorKind::VapidError(_) => StatusCode::UNAUTHORIZED, + ApiErrorKind::VapidError(_) | ApiErrorKind::Jwt(_) => StatusCode::UNAUTHORIZED, ApiErrorKind::InvalidToken | ApiErrorKind::InvalidApiVersion => StatusCode::NOT_FOUND, diff --git a/autoendpoint/src/server/extractors/subscription.rs b/autoendpoint/src/server/extractors/subscription.rs index 3c978b6c..371b0444 100644 --- a/autoendpoint/src/server/extractors/subscription.rs +++ b/autoendpoint/src/server/extractors/subscription.rs @@ -1,18 +1,20 @@ use crate::error::{ApiError, ApiErrorKind, ApiResult}; -use crate::server::extractors::token_info::TokenInfo; +use crate::server::extractors::token_info::{ApiVersion, TokenInfo}; use crate::server::extractors::user::validate_user; use crate::server::headers::crypto_key::CryptoKeyHeader; -use crate::server::headers::vapid::{VapidHeader, VapidVersionData}; +use crate::server::headers::vapid::{VapidHeader, VapidHeaderWithKey, VapidVersionData}; use crate::server::{ServerState, VapidError}; use actix_http::{Payload, PayloadStream}; use actix_web::web::Data; use actix_web::{FromRequest, HttpRequest}; use autopush_common::db::DynamoDbUser; +use autopush_common::util::sec_since_epoch; use cadence::{Counted, StatsdClient}; use futures::compat::Future01CompatExt; use futures::future::LocalBoxFuture; use futures::FutureExt; -use openssl::hash; +use jsonwebtoken::{Algorithm, DecodingKey, Validation}; +use openssl::hash::MessageDigest; use std::borrow::Cow; use uuid::Uuid; @@ -20,8 +22,7 @@ use uuid::Uuid; pub struct Subscription { pub user: DynamoDbUser, pub channel_id: Uuid, - pub vapid: Option, - pub public_key: Option, + pub vapid: Option, } impl FromRequest for Subscription { @@ -44,17 +45,14 @@ impl FromRequest for Subscription { .decrypt(&repad_base64(&token_info.token)) .map_err(|_| ApiErrorKind::InvalidToken)?; - // Parse VAPID and extract public key - let vapid = parse_vapid(&token_info, &state.metrics)?; - let public_key = vapid - .as_ref() + // Parse VAPID and extract public key. + let vapid: Option = parse_vapid(&token_info, &state.metrics)? .map(|vapid| extract_public_key(vapid, &token_info)) .transpose()?; - if token_info.api_version == "v2" { - version_2_validation(&token, public_key.as_deref())?; - } else { - version_1_validation(&token)?; + match token_info.api_version { + ApiVersion::Version1 => version_1_validation(&token)?, + ApiVersion::Version2 => version_2_validation(&token, vapid.as_ref())?, } // Load and validate user data @@ -68,11 +66,19 @@ impl FromRequest for Subscription { .map_err(ApiErrorKind::Database)?; validate_user(&user, &channel_id, &state).await?; + // Validate the VAPID JWT token and record the version + if let Some(vapid) = &vapid { + validate_vapid_jwt(vapid)?; + + state + .metrics + .incr(&format!("updates.vapid.draft{:02}", vapid.vapid.version()))?; + } + Ok(Subscription { user, channel_id, vapid, - public_key, }) } .boxed_local() @@ -115,8 +121,8 @@ fn parse_vapid(token_info: &TokenInfo, metrics: &StatsdClient) -> ApiResult ApiResult { - Ok(match &vapid.version_data { +fn extract_public_key(vapid: VapidHeader, token_info: &TokenInfo) -> ApiResult { + Ok(match vapid.version_data.clone() { VapidVersionData::Version1 => { // VAPID v1 stores the public key in the Crypto-Key header let header = token_info.crypto_key_header.as_deref().ok_or_else(|| { @@ -131,9 +137,12 @@ fn extract_public_key(vapid: &VapidHeader, token_info: &TokenInfo) -> ApiResult< ) })?; - public_key.to_string() + VapidHeaderWithKey { + vapid, + public_key: public_key.to_string(), + } } - VapidVersionData::Version2 { public_key } => public_key.clone(), + VapidVersionData::Version2 { public_key } => VapidHeaderWithKey { vapid, public_key }, }) } @@ -148,7 +157,7 @@ fn version_1_validation(token: &[u8]) -> ApiResult<()> { } /// `/webpush/v2/` validations -fn version_2_validation(token: &[u8], public_key: Option<&str>) -> ApiResult<()> { +fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> ApiResult<()> { if token.len() != 64 { // Corrupted token return Err(ApiErrorKind::InvalidToken.into()); @@ -157,12 +166,13 @@ fn version_2_validation(token: &[u8], public_key: Option<&str>) -> ApiResult<()> // Verify that the sender is authorized to send notifications. // The last 32 bytes of the token is the hashed public key. let token_key = &token[32..]; - let public_key = public_key.ok_or(ApiErrorKind::VapidError(VapidError::MissingKey))?; + let public_key = &vapid.ok_or(VapidError::MissingKey)?.public_key; // Hash the VAPID public key let public_key = base64::decode_config(public_key, base64::URL_SAFE_NO_PAD) .map_err(|_| VapidError::InvalidKey)?; - let key_hash = hash::hash(hash::MessageDigest::sha256(), &public_key)?; + let key_hash = openssl::hash::hash(MessageDigest::sha256(), &public_key) + .map_err(ApiErrorKind::TokenHashValidation)?; // Verify that the VAPID public key equals the (expected) token public key if !openssl::memcmp::eq(&key_hash, &token_key) { @@ -171,3 +181,36 @@ fn version_2_validation(token: &[u8], public_key: Option<&str>) -> ApiResult<()> Ok(()) } + +/// Validate the VAPID JWT token. Specifically, +/// - Check the signature +/// - Make sure it hasn't expired +/// - Make sure the expiration isn't too far into the future +/// +/// This is mostly taken care of by the jsonwebtoken library +fn validate_vapid_jwt(vapid: &VapidHeaderWithKey) -> ApiResult<()> { + let VapidHeaderWithKey { vapid, public_key } = vapid; + + #[derive(serde::Deserialize)] + struct Claims { + exp: u64, + } + + // Check the signature and make sure the expiration is in the future + let token_data = jsonwebtoken::decode::( + &vapid.token, + &DecodingKey::from_ec_der(public_key.as_bytes()), + &Validation::new(Algorithm::ES256), + )?; + + // Make sure the expiration isn't too far into the future + let now = sec_since_epoch(); + const ONE_DAY_IN_SECONDS: u64 = 60 * 60 * 24; + + if token_data.claims.exp - now > ONE_DAY_IN_SECONDS { + // The expiration is too far in the future + return Err(VapidError::FutureExpirationToken.into()); + } + + Ok(()) +} diff --git a/autoendpoint/src/server/extractors/token_info.rs b/autoendpoint/src/server/extractors/token_info.rs index 653463a5..19cadb65 100644 --- a/autoendpoint/src/server/extractors/token_info.rs +++ b/autoendpoint/src/server/extractors/token_info.rs @@ -3,10 +3,11 @@ use crate::server::headers::util::get_owned_header; use actix_http::{Payload, PayloadStream}; use actix_web::{FromRequest, HttpRequest}; use futures::future; +use std::str::FromStr; /// Extracts basic token data from the webpush request path and headers pub struct TokenInfo { - pub api_version: String, + pub api_version: ApiVersion, pub token: String, pub crypto_key_header: Option, pub auth_header: Option, @@ -19,23 +20,16 @@ impl FromRequest for TokenInfo { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { // Path variables - let api_version = req - .match_info() - .get("api_version") - .unwrap_or("v1") - .to_string(); + let api_version = match req.match_info().get("api_version").unwrap_or("v1").parse() { + Ok(version) => version, + Err(e) => return future::err(e), + }; let token = req .match_info() .get("token") .expect("{token} must be part of the webpush path") .to_string(); - // Check API version - match api_version.as_str() { - "v1" | "v2" => {} - _ => return future::err(ApiErrorKind::InvalidApiVersion.into()), - } - future::ok(TokenInfo { api_version, token, @@ -44,3 +38,21 @@ impl FromRequest for TokenInfo { }) } } + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ApiVersion { + Version1, + Version2, +} + +impl FromStr for ApiVersion { + type Err = ApiError; + + fn from_str(s: &str) -> Result { + match s { + "v1" => Ok(ApiVersion::Version1), + "v2" => Ok(ApiVersion::Version2), + _ => Err(ApiErrorKind::InvalidApiVersion.into()), + } + } +} diff --git a/autoendpoint/src/server/headers/vapid.rs b/autoendpoint/src/server/headers/vapid.rs index 00e17733..2d00eb74 100644 --- a/autoendpoint/src/server/headers/vapid.rs +++ b/autoendpoint/src/server/headers/vapid.rs @@ -12,8 +12,15 @@ pub struct VapidHeader { pub version_data: VapidVersionData, } +/// Combines the VAPID header details with the public key, which may not be from +/// the VAPID header +pub struct VapidHeaderWithKey { + pub vapid: VapidHeader, + pub public_key: String, +} + /// Version-specific VAPID data. Also used to identify the VAPID version. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum VapidVersionData { Version1, Version2 { public_key: String }, @@ -75,12 +82,12 @@ pub enum VapidError { MissingToken, #[error("Missing VAPID public key")] MissingKey, - #[error("Invalid VAPID token")] - InvalidToken, #[error("Invalid VAPID public key")] InvalidKey, #[error("VAPID public key mismatch")] KeyMismatch, + #[error("The VAPID token expiration is too long")] + FutureExpirationToken, #[error("Unknown auth scheme")] UnknownScheme, }