@@ -190,6 +190,21 @@ fn version_1_validation(token: &[u8]) -> ApiResult<()> {
190190 Ok ( ( ) )
191191}
192192
193+ /// Decode a public key string
194+ ///
195+ /// NOTE: Some customers send a VAPID public key with incorrect padding and
196+ /// in standard base64 encoding. (Both of these violate the VAPID RFC)
197+ /// Prior python versions ignored these errors, so we should too.
198+ fn decode_public_key ( public_key : & str ) -> ApiResult < Vec < u8 > > {
199+ let encoding = if public_key. contains ( [ '/' , '+' ] ) {
200+ base64:: STANDARD_NO_PAD
201+ } else {
202+ base64:: URL_SAFE_NO_PAD
203+ } ;
204+ base64:: decode_config ( public_key. trim_end_matches ( '=' ) , encoding)
205+ . map_err ( |e| VapidError :: InvalidKey ( e. to_string ( ) ) . into ( ) )
206+ }
207+
193208/// `/webpush/v2/` validations
194209fn version_2_validation ( token : & [ u8 ] , vapid : Option < & VapidHeaderWithKey > ) -> ApiResult < ( ) > {
195210 if token. len ( ) != 64 {
@@ -203,8 +218,7 @@ fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> Api
203218 let public_key = & vapid. ok_or ( VapidError :: MissingKey ) ?. public_key ;
204219
205220 // Hash the VAPID public key
206- let public_key = base64:: decode_config ( public_key, base64:: URL_SAFE_NO_PAD )
207- . map_err ( |e| VapidError :: InvalidKey ( e. to_string ( ) ) ) ?;
221+ let public_key = decode_public_key ( public_key) ?;
208222 let key_hash = openssl:: hash:: hash ( MessageDigest :: sha256 ( ) , & public_key)
209223 . map_err ( ApiErrorKind :: TokenHashValidation ) ?;
210224
@@ -225,20 +239,15 @@ fn version_2_validation(token: &[u8], vapid: Option<&VapidHeaderWithKey>) -> Api
225239fn validate_vapid_jwt ( vapid : & VapidHeaderWithKey , domain : & Url ) -> ApiResult < ( ) > {
226240 let VapidHeaderWithKey { vapid, public_key } = vapid;
227241
228- // Check the signature and make sure the expiration is in the future
229- // NOTE: FxA sometimes sends a VAPID public key with incorrect padding.
230- // Prior versions ignored padding errors, so we should too.
231- let public_key =
232- base64:: decode_config ( public_key. trim_end_matches ( '=' ) , base64:: URL_SAFE_NO_PAD )
233- . map_err ( |e| VapidError :: InvalidKey ( e. to_string ( ) ) ) ?;
234- // NOTE: This will fail if `exp` is specified as a string instead of a numeric.
242+ let public_key = decode_public_key ( public_key) ?;
235243 let token_data = match jsonwebtoken:: decode :: < VapidClaims > (
236244 & vapid. token ,
237245 & DecodingKey :: from_ec_der ( & public_key) ,
238246 & Validation :: new ( Algorithm :: ES256 ) ,
239247 ) {
240248 Ok ( v) => v,
241249 Err ( e) => match e. kind ( ) {
250+ // NOTE: This will fail if `exp` is specified as anything instead of a numeric or if a required field is empty
242251 jsonwebtoken:: errors:: ErrorKind :: Json ( e) => {
243252 if e. is_data ( ) {
244253 return Err ( VapidError :: InvalidVapid (
@@ -254,7 +263,7 @@ fn validate_vapid_jwt(vapid: &VapidHeaderWithKey, domain: &Url) -> ApiResult<()>
254263 } ,
255264 } ;
256265
257- // Make sure the expiration isn't too far into the future
266+ // Check the signature and make sure the expiration is in the future, but not too far
258267 if token_data. claims . exp > ( sec_since_epoch ( ) + ONE_DAY_IN_SECONDS ) {
259268 // The expiration is too far in the future
260269 return Err ( VapidError :: FutureExpirationToken . into ( ) ) ;
@@ -408,6 +417,76 @@ mod tests {
408417 ] )
409418 }
410419
420+ #[ test]
421+ fn vapid_public_key_variants ( ) {
422+ #[ derive( Debug , Deserialize , Serialize ) ]
423+ struct StrExpVapidClaims {
424+ exp : String ,
425+ aud : String ,
426+ sub : String ,
427+ }
428+
429+ let priv_key = base64:: decode_config (
430+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZImOgpRszunnU3j1\
431+ oX5UQiX8KU4X2OdbENuvc/t8wpmhRANCAATN21Y1v8LmQueGpSG6o022gTbbYa4l\
432+ bXWZXITsjknW1WHmELtouYpyXX7e41FiAMuDvcRwW2Nfehn/taHW/IXb",
433+ base64:: STANDARD ,
434+ )
435+ . unwrap ( ) ;
436+ // pretty much matches the kind of key we get from some partners.
437+ let public_key_standard = "BM3bVjW/wuZC54alIbqjTbaBNtthriVtdZlchOyOSdbVYeYQu2i5inJdft7jUWIAy4O9xHBbY196Gf+1odb8hds=" . to_owned ( ) ;
438+ let public_key_url_safe = "BM3bVjW_wuZC54alIbqjTbaBNtthriVtdZlchOyOSdbVYeYQu2i5inJdft7jUWIAy4O9xHBbY196Gf-1odb8hds=" . to_owned ( ) ;
439+ let domain = "https://push.services.mozilla.org" ;
440+ let jwk_header = jsonwebtoken:: Header :: new ( jsonwebtoken:: Algorithm :: ES256 ) ;
441+ let enc_key = jsonwebtoken:: EncodingKey :: from_ec_der ( & priv_key) ;
442+ let claims = VapidClaims {
443+ exp : sec_since_epoch ( ) + super :: ONE_DAY_IN_SECONDS - 100 ,
444+ aud : domain. to_owned ( ) ,
445+ sub : "mailto:admin@example.com" . to_owned ( ) ,
446+ } ;
447+ let token = jsonwebtoken:: encode ( & jwk_header, & claims, & enc_key) . unwrap ( ) ;
448+ // try standard form with padding
449+ let header = VapidHeaderWithKey {
450+ public_key : public_key_standard. clone ( ) ,
451+ vapid : VapidHeader {
452+ scheme : "vapid" . to_string ( ) ,
453+ token : token. clone ( ) ,
454+ version_data : VapidVersionData :: Version1 ,
455+ } ,
456+ } ;
457+ assert ! ( validate_vapid_jwt( & header, & Url :: from_str( domain) . unwrap( ) ) . is_ok( ) ) ;
458+ // try standard form with no padding
459+ let header = VapidHeaderWithKey {
460+ public_key : public_key_standard. trim_end_matches ( '=' ) . to_owned ( ) ,
461+ vapid : VapidHeader {
462+ scheme : "vapid" . to_string ( ) ,
463+ token : token. clone ( ) ,
464+ version_data : VapidVersionData :: Version1 ,
465+ } ,
466+ } ;
467+ assert ! ( validate_vapid_jwt( & header, & Url :: from_str( domain) . unwrap( ) ) . is_ok( ) ) ;
468+ // try URL safe form with padding
469+ let header = VapidHeaderWithKey {
470+ public_key : public_key_url_safe. clone ( ) ,
471+ vapid : VapidHeader {
472+ scheme : "vapid" . to_string ( ) ,
473+ token : token. clone ( ) ,
474+ version_data : VapidVersionData :: Version1 ,
475+ } ,
476+ } ;
477+ assert ! ( validate_vapid_jwt( & header, & Url :: from_str( domain) . unwrap( ) ) . is_ok( ) ) ;
478+ // try URL safe form without padding
479+ let header = VapidHeaderWithKey {
480+ public_key : public_key_url_safe. trim_end_matches ( '=' ) . to_owned ( ) ,
481+ vapid : VapidHeader {
482+ scheme : "vapid" . to_string ( ) ,
483+ token,
484+ version_data : VapidVersionData :: Version1 ,
485+ } ,
486+ } ;
487+ assert ! ( validate_vapid_jwt( & header, & Url :: from_str( domain) . unwrap( ) ) . is_ok( ) ) ;
488+ }
489+
411490 #[ test]
412491 fn vapid_missing_sub ( ) {
413492 #[ derive( Debug , Deserialize , Serialize ) ]
0 commit comments