Skip to content

Commit

Permalink
feat: allow for standard base64 private keys (#323)
Browse files Browse the repository at this point in the history
feat: allow for standard base64 private keys

FxA sends in standard encoded base64 private key fields. The old Python
verison accepted them. We need to accept them as well.
  • Loading branch information
jrconlin authored Jul 20, 2022
1 parent 3b88878 commit 7ec9e54
Showing 1 changed file with 89 additions and 10 deletions.
99 changes: 89 additions & 10 deletions autoendpoint/src/extractors/subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ fn version_1_validation(token: &[u8]) -> ApiResult<()> {
Ok(())
}

/// Decode a public key string
///
/// NOTE: Some customers send a VAPID public key with incorrect padding and
/// in standard base64 encoding. (Both of these violate the VAPID RFC)
/// Prior python versions ignored these errors, so we should too.
fn decode_public_key(public_key: &str) -> ApiResult<Vec<u8>> {
let encoding = if public_key.contains(['/', '+']) {
base64::STANDARD_NO_PAD
} else {
base64::URL_SAFE_NO_PAD
};
base64::decode_config(public_key.trim_end_matches('='), encoding)
.map_err(|e| VapidError::InvalidKey(e.to_string()).into())
}

/// `/webpush/v2/` validations
fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> ApiResult<()> {
if token.len() != 64 {
Expand All @@ -203,8 +218,7 @@ fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> Api
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(|e| VapidError::InvalidKey(e.to_string()))?;
let public_key = decode_public_key(public_key)?;
let key_hash = openssl::hash::hash(MessageDigest::sha256(), &public_key)
.map_err(ApiErrorKind::TokenHashValidation)?;

Expand All @@ -225,20 +239,15 @@ fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> Api
fn validate_vapid_jwt(vapid: &VapidHeaderWithKey, domain: &Url) -> ApiResult<()> {
let VapidHeaderWithKey { vapid, public_key } = vapid;

// Check the signature and make sure the expiration is in the future
// NOTE: FxA sometimes sends a VAPID public key with incorrect padding.
// Prior versions ignored padding errors, so we should too.
let public_key =
base64::decode_config(public_key.trim_end_matches('='), base64::URL_SAFE_NO_PAD)
.map_err(|e| VapidError::InvalidKey(e.to_string()))?;
// NOTE: This will fail if `exp` is specified as a string instead of a numeric.
let public_key = decode_public_key(public_key)?;
let token_data = match jsonwebtoken::decode::<VapidClaims>(
&vapid.token,
&DecodingKey::from_ec_der(&public_key),
&Validation::new(Algorithm::ES256),
) {
Ok(v) => v,
Err(e) => match e.kind() {
// NOTE: This will fail if `exp` is specified as anything instead of a numeric or if a required field is empty
jsonwebtoken::errors::ErrorKind::Json(e) => {
if e.is_data() {
return Err(VapidError::InvalidVapid(
Expand All @@ -254,7 +263,7 @@ fn validate_vapid_jwt(vapid: &VapidHeaderWithKey, domain: &Url) -> ApiResult<()>
},
};

// Make sure the expiration isn't too far into the future
// Check the signature and make sure the expiration is in the future, but not too far
if token_data.claims.exp > (sec_since_epoch() + ONE_DAY_IN_SECONDS) {
// The expiration is too far in the future
return Err(VapidError::FutureExpirationToken.into());
Expand Down Expand Up @@ -408,6 +417,76 @@ mod tests {
])
}

#[test]
fn vapid_public_key_variants() {
#[derive(Debug, Deserialize, Serialize)]
struct StrExpVapidClaims {
exp: String,
aud: String,
sub: String,
}

let priv_key = base64::decode_config(
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZImOgpRszunnU3j1\
oX5UQiX8KU4X2OdbENuvc/t8wpmhRANCAATN21Y1v8LmQueGpSG6o022gTbbYa4l\
bXWZXITsjknW1WHmELtouYpyXX7e41FiAMuDvcRwW2Nfehn/taHW/IXb",
base64::STANDARD,
)
.unwrap();
// pretty much matches the kind of key we get from some partners.
let public_key_standard = "BM3bVjW/wuZC54alIbqjTbaBNtthriVtdZlchOyOSdbVYeYQu2i5inJdft7jUWIAy4O9xHBbY196Gf+1odb8hds=".to_owned();
let public_key_url_safe = "BM3bVjW_wuZC54alIbqjTbaBNtthriVtdZlchOyOSdbVYeYQu2i5inJdft7jUWIAy4O9xHBbY196Gf-1odb8hds=".to_owned();
let domain = "https://push.services.mozilla.org";
let jwk_header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256);
let enc_key = jsonwebtoken::EncodingKey::from_ec_der(&priv_key);
let claims = VapidClaims {
exp: sec_since_epoch() + super::ONE_DAY_IN_SECONDS - 100,
aud: domain.to_owned(),
sub: "mailto:[email protected]".to_owned(),
};
let token = jsonwebtoken::encode(&jwk_header, &claims, &enc_key).unwrap();
// try standard form with padding
let header = VapidHeaderWithKey {
public_key: public_key_standard.clone(),
vapid: VapidHeader {
scheme: "vapid".to_string(),
token: token.clone(),
version_data: VapidVersionData::Version1,
},
};
assert!(validate_vapid_jwt(&header, &Url::from_str(domain).unwrap()).is_ok());
// try standard form with no padding
let header = VapidHeaderWithKey {
public_key: public_key_standard.trim_end_matches('=').to_owned(),
vapid: VapidHeader {
scheme: "vapid".to_string(),
token: token.clone(),
version_data: VapidVersionData::Version1,
},
};
assert!(validate_vapid_jwt(&header, &Url::from_str(domain).unwrap()).is_ok());
// try URL safe form with padding
let header = VapidHeaderWithKey {
public_key: public_key_url_safe.clone(),
vapid: VapidHeader {
scheme: "vapid".to_string(),
token: token.clone(),
version_data: VapidVersionData::Version1,
},
};
assert!(validate_vapid_jwt(&header, &Url::from_str(domain).unwrap()).is_ok());
// try URL safe form without padding
let header = VapidHeaderWithKey {
public_key: public_key_url_safe.trim_end_matches('=').to_owned(),
vapid: VapidHeader {
scheme: "vapid".to_string(),
token,
version_data: VapidVersionData::Version1,
},
};
assert!(validate_vapid_jwt(&header, &Url::from_str(domain).unwrap()).is_ok());
}

#[test]
fn vapid_missing_sub() {
#[derive(Debug, Deserialize, Serialize)]
Expand Down

0 comments on commit 7ec9e54

Please sign in to comment.