From 0211ad4a1a9ef8cf3f6cb0361d16afa1a317af46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Thu, 19 Dec 2019 15:35:19 -0500 Subject: [PATCH 1/5] picky: update JWT API + check nbf claim if present --- picky/src/jose/jwt.rs | 461 +++++++++++++++++++++++------- picky/src/key.rs | 4 + test_assets/jose/jwt_with_exp.txt | 2 +- 3 files changed, 357 insertions(+), 110 deletions(-) diff --git a/picky/src/jose/jwt.rs b/picky/src/jose/jwt.rs index 7b608318..b72f5990 100644 --- a/picky/src/jose/jwt.rs +++ b/picky/src/jose/jwt.rs @@ -10,6 +10,7 @@ use std::borrow::Cow; // === error type === // #[derive(Debug, Snafu)] +#[non_exhaustive] pub enum JwtError { /// RSA error #[snafu(display("RSA error: {}", context))] @@ -42,13 +43,25 @@ pub enum JwtError { #[snafu(display("header says input is not a JWT: expected JWT, found {}", typ))] UnexpectedType { typ: String }, + /// registered claim type is invalid + #[snafu(display("registered claim `{}` has invalid type", claim))] + InvalidRegisteredClaimType { claim: &'static str }, + /// a required claim is missing - #[snafu(display("{} claim is required but is missing", claim))] - MissingClaim { claim: &'static str }, + #[snafu(display("required claim `{}` is missing", claim))] + RequiredClaimMissing { claim: &'static str }, + + /// token not yet valid + #[snafu(display("token not yet valid (not before: {}, now: {} [leeway: {}])", not_before, now.numeric_date, now.leeway))] + NotYetValid { not_before: i64, now: JwtDate }, /// token expired #[snafu(display("token expired (not after: {}, now: {} [leeway: {}])", not_after, now.numeric_date, now.leeway))] Expired { not_after: i64, now: JwtDate }, + + /// validator is invalid + #[snafu(display("invalid validator: {}", description))] + InvalidValidator { description: &'static str }, } impl From for JwtError { @@ -84,33 +97,162 @@ impl From for JwtError { #[derive(Clone, Debug)] pub struct JwtDate { pub numeric_date: i64, - pub leeway: u8, + pub leeway: u16, } impl JwtDate { - pub fn new(numeric_date: i64) -> Self { + pub const fn new(numeric_date: i64) -> Self { Self { numeric_date, leeway: 0, } } - pub fn new_with_leeway(numeric_date: i64, leeway: u8) -> Self { + pub const fn new_with_leeway(numeric_date: i64, leeway: u16) -> Self { Self { numeric_date, leeway } } - pub fn is_before(&self, other_numeric_date: i64) -> bool { + pub const fn is_before(&self, other_numeric_date: i64) -> bool { + self.numeric_date <= other_numeric_date + self.leeway as i64 + } + + pub const fn is_before_strict(&self, other_numeric_date: i64) -> bool { self.numeric_date < other_numeric_date + self.leeway as i64 } - pub fn is_after(&self, other_numeric_date: i64) -> bool { + pub const fn is_after(&self, other_numeric_date: i64) -> bool { + self.numeric_date >= other_numeric_date - self.leeway as i64 + } + + pub const fn is_after_strict(&self, other_numeric_date: i64) -> bool { self.numeric_date > other_numeric_date - self.leeway as i64 } } +// === validator === // + +#[derive(Debug, Clone, Copy)] +enum CheckStrictness { + Ignored, + Optional, + Required, +} + +#[derive(Debug, Clone)] +pub struct JwtValidator<'a> { + public_key: Option<&'a PublicKey<'a>>, + current_date: Option<&'a JwtDate>, + expiration_claim: CheckStrictness, + not_before_claim: CheckStrictness, +} + +pub const DANGEROUS_VALIDATOR: JwtValidator<'static> = JwtValidator::dangerous(); + +impl<'a> JwtValidator<'a> { + /// Check signature and the registered exp and nbf claims. If a claim is missing token is rejected. + pub const fn strict(public_key: &'a PublicKey<'a>, current_date: &'a JwtDate) -> Self { + Self { + public_key: Some(public_key), + current_date: Some(current_date), + expiration_claim: CheckStrictness::Required, + not_before_claim: CheckStrictness::Required, + } + } + + /// Check signature and the registered exp and nbf claims. Token isn't rejected if a claim is missing. + pub const fn lenient(public_key: &'a PublicKey<'a>, current_date: &'a JwtDate) -> Self { + Self { + public_key: Some(public_key), + current_date: Some(current_date), + expiration_claim: CheckStrictness::Optional, + not_before_claim: CheckStrictness::Optional, + } + } + + /// Check signature only. No registered claim is checked. + pub const fn signature_only(public_key: &'a PublicKey<'a>) -> Self { + Self { + public_key: Some(public_key), + current_date: None, + expiration_claim: CheckStrictness::Ignored, + not_before_claim: CheckStrictness::Ignored, + } + } + + /// No check. + pub const fn dangerous() -> Self { + Self { + public_key: None, + current_date: None, + expiration_claim: CheckStrictness::Ignored, + not_before_claim: CheckStrictness::Ignored, + } + } + + pub fn public_key(self, public_key: &'a PublicKey<'a>) -> Self { + Self { + public_key: Some(public_key), + ..self + } + } + + pub fn current_date(self, current_date: &'a JwtDate) -> Self { + Self { + current_date: Some(current_date), + expiration_claim: CheckStrictness::Required, + not_before_claim: CheckStrictness::Required, + ..self + } + } + + pub fn expiration_check_required(self) -> Self { + Self { + expiration_claim: CheckStrictness::Required, + ..self + } + } + + pub fn expiration_check_optional(self) -> Self { + Self { + expiration_claim: CheckStrictness::Optional, + ..self + } + } + + pub fn expiration_check_ignored(self) -> Self { + Self { + expiration_claim: CheckStrictness::Ignored, + ..self + } + } + + pub fn not_before_check_required(self) -> Self { + Self { + not_before_claim: CheckStrictness::Required, + ..self + } + } + + pub fn not_before_check_optional(self) -> Self { + Self { + not_before_claim: CheckStrictness::Optional, + ..self + } + } + + pub fn not_before_check_ignored(self) -> Self { + Self { + not_before_claim: CheckStrictness::Ignored, + ..self + } + } +} + // === json web token === // const JWT_TYPE: &str = "JWT"; +const EXPIRATION_TIME_CLAIM: &str = "exp"; +const NOT_BEFORE_CLAIM: &str = "nbf"; #[derive(Serialize, Deserialize, Debug)] struct Header<'a> { @@ -147,12 +289,11 @@ impl<'a, C> Jwt<'a, C> { input: encoded_token.to_owned(), })?; - snafu::ensure!( - last_dot_idx + 1 < encoded_token.len(), - InvalidEncoding { + if encoded_token.ends_with('.') { + return Err(JwtError::InvalidEncoding { input: encoded_token.to_owned(), - } - ); + }); + } let signature = base64::decode_config(&encoded_token[last_dot_idx + 1..], base64::URL_SAFE_NO_PAD)?; @@ -176,85 +317,107 @@ impl<'a, C: Serialize> Jwt<'a, C> { } impl<'a, C: DeserializeOwned> Jwt<'a, C> { - /// Decode JWT and check the registered exp claim. - pub fn decode_with_exp_check( - encoded_token: &str, - public_key: &PublicKey, - current_date: &JwtDate, - ) -> Result { - decode_impl(encoded_token, Some(public_key), Some(current_date)) - } - - /// Basic JWT decoding method. - /// - /// This doesn't check expiration time. - pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result { - decode_impl(encoded_token, Some(public_key), None) - } - - /// Unsafe JWT decoding method. Signature isn't checked at all. - pub fn decode_without_signature_check(encoded_token: &str) -> Result { - decode_impl(encoded_token, None, None) - } -} + /// Validate using validator and returns decoded JWT. + pub fn decode(encoded_token: &str, validator: &JwtValidator) -> Result { + let first_dot_idx = encoded_token.find('.').ok_or_else(|| JwtError::InvalidEncoding { + input: encoded_token.to_owned(), + })?; -fn decode_impl<'a, C: DeserializeOwned>( - encoded_token: &str, - public_key: Option<&PublicKey>, - current_date: Option<&JwtDate>, -) -> Result, JwtError> { - let first_dot_idx = encoded_token.find('.').ok_or_else(|| JwtError::InvalidEncoding { - input: encoded_token.to_owned(), - })?; - - let last_dot_idx = encoded_token.rfind('.').ok_or_else(|| JwtError::InvalidEncoding { - input: encoded_token.to_owned(), - })?; - - snafu::ensure!( - first_dot_idx != last_dot_idx && last_dot_idx + 1 < encoded_token.len(), - InvalidEncoding { + let last_dot_idx = encoded_token.rfind('.').ok_or_else(|| JwtError::InvalidEncoding { input: encoded_token.to_owned(), + })?; + + if first_dot_idx == last_dot_idx || encoded_token.starts_with('.') || encoded_token.ends_with('.') { + return Err(JwtError::InvalidEncoding { + input: encoded_token.to_owned(), + }); } - ); - let header_json = base64::decode_config(&encoded_token[..first_dot_idx], base64::URL_SAFE_NO_PAD)?; - let header = serde_json::from_slice::
(&header_json)?; + let header_json = base64::decode_config(&encoded_token[..first_dot_idx], base64::URL_SAFE_NO_PAD)?; + let header = serde_json::from_slice::
(&header_json)?; - snafu::ensure!( - header.typ == JWT_TYPE, - UnexpectedType { - typ: header.typ.to_owned(), + if header.typ != JWT_TYPE { + return Err(JwtError::UnexpectedType { typ: header.typ.into() }); } - ); - if let Some(public_key) = public_key { - let signature = base64::decode_config(&encoded_token[last_dot_idx + 1..], base64::URL_SAFE_NO_PAD)?; + if let Some(public_key) = &validator.public_key { + let signature = base64::decode_config(&encoded_token[last_dot_idx + 1..], base64::URL_SAFE_NO_PAD)?; - header - .alg - .verify(public_key, &encoded_token[..last_dot_idx].as_bytes(), &signature)?; - } + header + .alg + .verify(public_key, &encoded_token[..last_dot_idx].as_bytes(), &signature)?; + } - let claims_json = base64::decode_config(&encoded_token[first_dot_idx + 1..last_dot_idx], base64::URL_SAFE_NO_PAD)?; - let claims = if let Some(current_date) = current_date { - let claims = serde_json::from_slice::(&claims_json)?; - let exp = claims["exp"] - .as_i64() - .ok_or_else(|| JwtError::MissingClaim { claim: "exp" })?; - snafu::ensure!( - current_date.is_before(exp), - Expired { - not_after: exp, - now: current_date.clone(), + let claims_json = + base64::decode_config(&encoded_token[first_dot_idx + 1..last_dot_idx], base64::URL_SAFE_NO_PAD)?; + + let claims = match ( + validator.current_date, + validator.not_before_claim, + validator.expiration_claim, + ) { + (None, CheckStrictness::Required, _) | (None, _, CheckStrictness::Required) => { + return Err(JwtError::InvalidValidator { + description: "current date is missing", + }) } - ); - serde_json::value::from_value(claims)? - } else { - serde_json::from_slice(&claims_json)? - }; + (Some(current_date), nbf_strictness, exp_strictness) => { + let claims = serde_json::from_slice::(&claims_json)?; + + let nbf_opt = claims.get(NOT_BEFORE_CLAIM); + match (nbf_strictness, nbf_opt) { + (CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {} + (CheckStrictness::Required, None) => { + return Err(JwtError::RequiredClaimMissing { + claim: NOT_BEFORE_CLAIM, + }) + } + (_, Some(nbf)) => { + let nbf_i64 = nbf.as_i64().ok_or_else(|| JwtError::InvalidRegisteredClaimType { + claim: NOT_BEFORE_CLAIM, + })?; + if !current_date.is_after(nbf_i64) { + return Err(JwtError::NotYetValid { + not_before: nbf_i64, + now: current_date.clone(), + }); + } + } + } + + let exp_opt = claims.get(EXPIRATION_TIME_CLAIM); + match (exp_strictness, exp_opt) { + (CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {} + (CheckStrictness::Required, None) => { + return Err(JwtError::RequiredClaimMissing { + claim: EXPIRATION_TIME_CLAIM, + }) + } + (_, Some(exp)) => { + let exp_i64 = exp.as_i64().ok_or_else(|| JwtError::InvalidRegisteredClaimType { + claim: EXPIRATION_TIME_CLAIM, + })?; + if !current_date.is_before_strict(exp_i64) { + return Err(JwtError::Expired { + not_after: exp_i64, + now: current_date.clone(), + }); + } + } + } + + serde_json::value::from_value(claims)? + } + (None, _, _) => serde_json::from_slice(&claims_json)?, + }; + + Ok(Jwt { header, claims }) + } - Ok(Jwt { header, claims }) + /// Unsafe JWT decoding method. Signature isn't checked at all. + pub fn decode_without_validation(encoded_token: &str) -> Result { + Self::decode(encoded_token, &DANGEROUS_VALIDATOR) + } } #[cfg(test)] @@ -280,7 +443,7 @@ mod tests { PrivateKey::from_pkcs8(pk_pem.data()).unwrap() } - fn get_strongly_typed_claims() -> MyClaims { + const fn get_strongly_typed_claims() -> MyClaims { MyClaims { sub: Cow::Borrowed("1234567890"), name: Cow::Borrowed("John Doe"), @@ -300,15 +463,48 @@ mod tests { #[test] fn decode_rsa_sha256() { let public_key = get_private_key_1().to_public_key(); - let jwt = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &public_key).unwrap(); + let validator = JwtValidator::signature_only(&public_key); + let jwt = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &validator).unwrap(); let claims = jwt.into_claims(); assert_eq!(claims, get_strongly_typed_claims()); + + // exp and nbf claims aren't present but this should pass with lenient validator + let now = JwtDate::new(0); + let validator = validator + .current_date(&now) + .expiration_check_optional() + .not_before_check_optional(); + Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &validator).unwrap(); + } + + #[test] + fn decode_invalid_validator_err() { + let public_key = get_private_key_1().to_public_key(); + let validator = JwtValidator::signature_only(&public_key) + .expiration_check_required() + .not_before_check_optional(); + let err = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &validator) + .err() + .unwrap(); + assert_eq!(err.to_string(), "invalid validator: current date is missing"); + } + + #[test] + fn decode_required_claim_missing_err() { + let public_key = get_private_key_1().to_public_key(); + let now = JwtDate::new(0); + let validator = JwtValidator::strict(&public_key, &now); + let err = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &validator) + .err() + .unwrap(); + assert_eq!(err.to_string(), "required claim `nbf` is missing"); } #[test] fn decode_rsa_sha256_using_json_value_claims() { let public_key = get_private_key_1().to_public_key(); - let jwt = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &public_key).unwrap(); + let validator = JwtValidator::signature_only(&public_key); + let jwt = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &validator).unwrap(); let claims = jwt.into_claims(); assert_eq!(claims["sub"].as_str().expect("sub"), "1234567890"); assert_eq!(claims["name"].as_str().expect("name"), "John Doe"); @@ -318,7 +514,7 @@ mod tests { #[test] fn decode_rsa_sha256_delayed_signature_check() { - let jwt = Jwt::::decode_without_signature_check(crate::test_files::JOSE_JWT_EXAMPLE).unwrap(); + let jwt = Jwt::::decode_without_validation(crate::test_files::JOSE_JWT_EXAMPLE).unwrap(); let claims = jwt.view_claims(); assert_eq!(claims, &get_strongly_typed_claims()); @@ -333,16 +529,21 @@ mod tests { #[test] fn decode_rsa_sha256_invalid_signature_err() { let public_key = get_private_key_2().to_public_key(); - let err = Jwt::::decode(crate::test_files::JOSE_JWT_EXAMPLE, &public_key) - .err() - .unwrap(); + let err = Jwt::::decode( + crate::test_files::JOSE_JWT_EXAMPLE, + &JwtValidator::signature_only(&public_key), + ) + .err() + .unwrap(); assert_eq!(err.to_string(), "signature error: invalid signature"); } #[test] fn decode_invalid_base64_err() { let public_key = get_private_key_1().to_public_key(); - let err = Jwt::::decode("aieoè~†.tésp.à", &public_key).err().unwrap(); + let err = Jwt::::decode("aieoè~†.tésp.à", &JwtValidator::signature_only(&public_key)) + .err() + .unwrap(); assert_eq!(err.to_string(), "couldn\'t decode base64: Invalid byte 195, offset 4."); } @@ -350,12 +551,17 @@ mod tests { fn decode_invalid_json_err() { let public_key = get_private_key_1().to_public_key(); - let err = Jwt::::decode("abc.abc.abc", &public_key).err().unwrap(); - assert_eq!(err.to_string(), "JSON error: expected value at line 1 column 1"); - - let err = Jwt::::decode("eyAiYWxnIjogIkhTMjU2IH0K.abc.abc", &public_key) + let err = Jwt::::decode("abc.abc.abc", &JwtValidator::signature_only(&public_key)) .err() .unwrap(); + assert_eq!(err.to_string(), "JSON error: expected value at line 1 column 1"); + + let err = Jwt::::decode( + "eyAiYWxnIjogIkhTMjU2IH0K.abc.abc", + &JwtValidator::signature_only(&public_key), + ) + .err() + .unwrap(); assert_eq!( err.to_string(), "JSON error: control character (\\u0000-\\u001F) \ @@ -367,19 +573,31 @@ mod tests { fn decode_invalid_encoding_err() { let public_key = get_private_key_1().to_public_key(); - let err = Jwt::::decode("abc.abc.", &public_key).err().unwrap(); + let err = Jwt::::decode(".abc.abc", &JwtValidator::signature_only(&public_key)) + .err() + .unwrap(); + assert_eq!(err.to_string(), "input isn\'t a valid token string: .abc.abc"); + + let err = Jwt::::decode("abc.abc.", &JwtValidator::signature_only(&public_key)) + .err() + .unwrap(); assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc."); - let err = Jwt::::decode("abc.abc", &public_key).err().unwrap(); + let err = Jwt::::decode("abc.abc", &JwtValidator::signature_only(&public_key)) + .err() + .unwrap(); assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc"); - let err = Jwt::::decode("abc", &public_key).err().unwrap(); + let err = Jwt::::decode("abc", &JwtValidator::signature_only(&public_key)) + .err() + .unwrap(); assert_eq!(err.to_string(), "input isn\'t a valid token string: abc"); } #[derive(Serialize, Deserialize)] struct MyExpirableClaims { exp: i64, + nbf: i64, msg: String, } @@ -387,32 +605,57 @@ mod tests { fn decode_jwt_not_expired() { let public_key = get_private_key_1().to_public_key(); - let jwt = Jwt::::decode_with_exp_check( + let jwt = Jwt::::decode( crate::test_files::JOSE_JWT_WITH_EXP, - &public_key, - &JwtDate::new(1545263999), - ).expect("couldn't decode jwt without leeway"); + &JwtValidator::strict(&public_key, &JwtDate::new(1545263999)), + ) + .expect("couldn't decode jwt without leeway"); + let claims = jwt.into_claims(); assert_eq!(claims.exp, 1545264000); + assert_eq!(claims.nbf, 1545263000); assert_eq!(claims.msg, "THIS IS TIME SENSITIVE DATA"); // alternatively, a leeway can account for small clock skew - Jwt::::decode_with_exp_check( + Jwt::::decode( crate::test_files::JOSE_JWT_WITH_EXP, - &public_key, - &JwtDate::new_with_leeway(1545264001, 10), - ).expect("couldn't decode jwt with leeway"); + &JwtValidator::strict(&public_key, &JwtDate::new_with_leeway(1545264001, 10)), + ) + .expect("couldn't decode jwt with leeway for exp"); + + Jwt::::decode( + crate::test_files::JOSE_JWT_WITH_EXP, + &JwtValidator::strict(&public_key, &JwtDate::new_with_leeway(1545262999, 10)), + ) + .expect("couldn't decode jwt with leeway for nbf"); } #[test] - fn decode_jwt_expired_err() { + fn decode_jwt_invalid_date_err() { let public_key = get_private_key_1().to_public_key(); - let err = Jwt::::decode_with_exp_check( + let err = Jwt::::decode( + crate::test_files::JOSE_JWT_WITH_EXP, + &JwtValidator::strict(&public_key, &JwtDate::new(1545264001)), + ) + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + "token expired (not after: 1545264000, now: 1545264001 [leeway: 0])" + ); + + let err = Jwt::::decode( crate::test_files::JOSE_JWT_WITH_EXP, - &public_key, - &JwtDate::new(1545264001), - ).err().unwrap(); - assert_eq!(err.to_string(), "token expired (not after: 1545264000, now: 1545264001 [leeway: 0])"); + &JwtValidator::strict(&public_key, &JwtDate::new_with_leeway(1545262998, 1)), + ) + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + "token not yet valid (not before: 1545263000, now: 1545262998 [leeway: 1])" + ); } } diff --git a/picky/src/key.rs b/picky/src/key.rs index 26ea14ca..048d58de 100644 --- a/picky/src/key.rs +++ b/picky/src/key.rs @@ -221,6 +221,10 @@ impl From for OwnedPublicKey { } impl OwnedPublicKey { + pub fn as_borrowed(&self) -> PublicKey<'_> { + PublicKey(PublicKeyInner::Ref(self.0.as_ref())) + } + pub fn from_pem(pem: &Pem) -> Result { match pem.label() { PUBLIC_KEY_PEM_LABEL => Self::from_der(pem.data()), diff --git a/test_assets/jose/jwt_with_exp.txt b/test_assets/jose/jwt_with_exp.txt index 82356cf6..248ca752 100644 --- a/test_assets/jose/jwt_with_exp.txt +++ b/test_assets/jose/jwt_with_exp.txt @@ -1 +1 @@ -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUyNjQwMDAsIm1zZyI6IlRISVMgSVMgVElNRSBTRU5TSVRJVkUgREFUQSJ9.jJKXI9RaGt_ZqEOZlC62RaW8flWrV7UusDYSXrAoAtZMNpn--Ja1LnZ5R-hmSSiT-yq4zliu98dctSS3uU3jtkiVVSd60A8IjlLUtd0RRJ90YG53qP-Nh0SnRSz-ScPgEuqrSwWinJ0D0u12MxIzpKLoqL9bPvGOZYTtVl3e9pb-b5pD-O25t1WLLgLdO_AoG-kZ6InpxqOZi58GQy1hPDBTlvOdabkOPPgLtYlyo6FCP4TR6To0lB4mg5RDl85DwrK3YSIcKumTHMvrE9w0aibbf2edYrllT5-FLJuRU5DniMjSDzn1DcMpaGqCMJ4ykDnXc7gvwWcQJxNYRKLLjA \ No newline at end of file +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUyNjQwMDAsIm5iZiI6MTU0NTI2MzAwMCwibXNnIjoiVEhJUyBJUyBUSU1FIFNFTlNJVElWRSBEQVRBIn0.X0ZDfwWChqTgZAmdM4n7qLB1CuY2HabhQ-XteOnfZ0riMdVUhN1M7LGfuZN5kOrFSulRG6A5VZTKiP8QaZSWIOdUXd11cDVpVjH_JNbMVyts4DnuIv2XYeyCAsbUklZsKb0sgRZTG07MQXm_TVbdUUsgvhS5Mwqh_qPkS4NkugyXMNPNodxJUxT_DGPLBDGugyFoaEiHfkjJ7wulq7ldYYiXAPvRv52vgMHUK8K1VhrWgguw8OGqY1r1tc762yNrU1qK1L7_6b5BUEJNW_xIZlT9y9d2pxF5cWbF8bYle_WR_282GyAzrXBIcmaPsO3cVnsJzuS8FAwN-kGaTyrfPg \ No newline at end of file From 9a0dd376728b550ef27afe3fa799126ed3b2054e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Thu, 19 Dec 2019 15:35:37 -0500 Subject: [PATCH 2/5] picky: bump version --- Cargo.lock | 2 +- picky/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea8e5549..7cb95299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1245,7 +1245,7 @@ dependencies = [ [[package]] name = "picky" -version = "4.2.1" +version = "4.4.2" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/picky/Cargo.toml b/picky/Cargo.toml index cb56b046..05f5108e 100644 --- a/picky/Cargo.toml +++ b/picky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "picky" -version = "4.2.1" +version = "4.4.2" authors = [ "jtrepanier-devolutions ", "Benoît CORTIER ", From c3b650738b58b0a0dafa8691e5fe2283dd377d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 20 Dec 2019 22:50:52 -0500 Subject: [PATCH 3/5] =?UTF-8?q?picky:=20PublicKey=20borrowing=20API=20made?= =?UTF-8?q?=20right=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- picky/src/jose/jwk.rs | 4 +- picky/src/jose/jwt.rs | 10 +-- picky/src/key.rs | 128 +++++++++++++--------------------- picky/src/x509/certificate.rs | 8 +-- picky/src/x509/csr.rs | 6 +- 5 files changed, 62 insertions(+), 94 deletions(-) diff --git a/picky/src/jose/jwk.rs b/picky/src/jose/jwk.rs index 64db7dc1..569f89a5 100644 --- a/picky/src/jose/jwk.rs +++ b/picky/src/jose/jwk.rs @@ -1,5 +1,5 @@ use crate::{ - key::{OwnedPublicKey, PublicKey}, + key::PublicKey, private::SubjectPublicKeyInfo, signature::SignatureHashType, }; @@ -177,7 +177,7 @@ impl Jwk { Ok(serde_json::to_string_pretty(self)?) } - pub fn to_public_key(&self) -> Result { + pub fn to_public_key(&self) -> Result { match &self.key { JwkKeyType::Rsa(rsa) => { let spki = SubjectPublicKeyInfo::new_rsa_key(rsa.modulus()?.into(), rsa.public_exponent()?.into()); diff --git a/picky/src/jose/jwt.rs b/picky/src/jose/jwt.rs index b72f5990..24fff2c4 100644 --- a/picky/src/jose/jwt.rs +++ b/picky/src/jose/jwt.rs @@ -140,7 +140,7 @@ enum CheckStrictness { #[derive(Debug, Clone)] pub struct JwtValidator<'a> { - public_key: Option<&'a PublicKey<'a>>, + public_key: Option<&'a PublicKey>, current_date: Option<&'a JwtDate>, expiration_claim: CheckStrictness, not_before_claim: CheckStrictness, @@ -150,7 +150,7 @@ pub const DANGEROUS_VALIDATOR: JwtValidator<'static> = JwtValidator::dangerous() impl<'a> JwtValidator<'a> { /// Check signature and the registered exp and nbf claims. If a claim is missing token is rejected. - pub const fn strict(public_key: &'a PublicKey<'a>, current_date: &'a JwtDate) -> Self { + pub const fn strict(public_key: &'a PublicKey, current_date: &'a JwtDate) -> Self { Self { public_key: Some(public_key), current_date: Some(current_date), @@ -160,7 +160,7 @@ impl<'a> JwtValidator<'a> { } /// Check signature and the registered exp and nbf claims. Token isn't rejected if a claim is missing. - pub const fn lenient(public_key: &'a PublicKey<'a>, current_date: &'a JwtDate) -> Self { + pub const fn lenient(public_key: &'a PublicKey, current_date: &'a JwtDate) -> Self { Self { public_key: Some(public_key), current_date: Some(current_date), @@ -170,7 +170,7 @@ impl<'a> JwtValidator<'a> { } /// Check signature only. No registered claim is checked. - pub const fn signature_only(public_key: &'a PublicKey<'a>) -> Self { + pub const fn signature_only(public_key: &'a PublicKey) -> Self { Self { public_key: Some(public_key), current_date: None, @@ -189,7 +189,7 @@ impl<'a> JwtValidator<'a> { } } - pub fn public_key(self, public_key: &'a PublicKey<'a>) -> Self { + pub fn public_key(self, public_key: &'a PublicKey) -> Self { Self { public_key: Some(public_key), ..self diff --git a/picky/src/key.rs b/picky/src/key.rs index 048d58de..54ceed84 100644 --- a/picky/src/key.rs +++ b/picky/src/key.rs @@ -111,7 +111,7 @@ impl PrivateKey { Ok(to_pem(PRIVATE_KEY_PEM_LABEL, &self.to_pkcs8()?)) } - pub fn to_public_key(&self) -> OwnedPublicKey { + pub fn to_public_key(&self) -> PublicKey { match &self.0.private_key { PrivateKeyValue::RSA(OctetStringAsn1Container(key)) => { SubjectPublicKeyInfo::new_rsa_key(key.modulus().clone(), key.public_exponent().clone()).into() @@ -150,79 +150,68 @@ impl PrivateKey { const PUBLIC_KEY_PEM_LABEL: &str = "PUBLIC KEY"; const RSA_PUBLIC_KEY_PEM_LABEL: &str = "RSA PUBLIC KEY"; -#[derive(Debug, Clone, PartialEq)] -enum PublicKeyInner<'a> { - Owned(SubjectPublicKeyInfo), - Ref(&'a SubjectPublicKeyInfo), -} - -impl<'a> From for PublicKeyInner<'a> { - fn from(spki: SubjectPublicKeyInfo) -> Self { - Self::Owned(spki) - } -} +#[derive(Clone, Debug, PartialEq)] +#[repr(transparent)] +pub struct PublicKey(SubjectPublicKeyInfo); -impl<'a> From<&'a SubjectPublicKeyInfo> for PublicKeyInner<'a> { +impl<'a> From<&'a SubjectPublicKeyInfo> for &'a PublicKey { + #[inline] fn from(spki: &'a SubjectPublicKeyInfo) -> Self { - Self::Ref(spki) + unsafe { &*(spki as *const SubjectPublicKeyInfo as *const PublicKey) } } } -impl<'a> PublicKeyInner<'a> { - pub fn into_owned(self) -> SubjectPublicKeyInfo { - match self { - PublicKeyInner::Owned(spki) => spki, - PublicKeyInner::Ref(spki) => (*spki).clone(), - } +impl<'a> From<&'a PublicKey> for &'a SubjectPublicKeyInfo { + #[inline] + fn from(key: &'a PublicKey) -> Self { + unsafe { &*(key as *const PublicKey as *const SubjectPublicKeyInfo) } } } -impl<'a> AsRef for PublicKeyInner<'a> { - fn as_ref(&self) -> &SubjectPublicKeyInfo { - match self { - PublicKeyInner::Owned(spki) => spki, - PublicKeyInner::Ref(spki) => spki, - } +impl From for PublicKey { + #[inline] + fn from(spki: SubjectPublicKeyInfo) -> Self { + Self(spki) } } -#[derive(Clone, Debug, PartialEq)] -pub struct PublicKey<'a>(PublicKeyInner<'a>); -pub type OwnedPublicKey = PublicKey<'static>; - -impl From for OwnedPublicKey { - fn from(key: SubjectPublicKeyInfo) -> Self { - Self(PublicKeyInner::Owned(key)) +impl From for SubjectPublicKeyInfo { + #[inline] + fn from(key: PublicKey) -> Self { + key.0 } } -impl<'a> From<&'a SubjectPublicKeyInfo> for PublicKey<'a> { - fn from(key: &'a SubjectPublicKeyInfo) -> Self { - Self(PublicKeyInner::Ref(key)) +impl From for PublicKey { + #[inline] + fn from(key: PrivateKey) -> Self { + Self(key.into()) } } -impl From for SubjectPublicKeyInfo { - fn from(key: OwnedPublicKey) -> Self { - key.0.into_owned() +impl AsRef for PublicKey { + #[inline] + fn as_ref(&self) -> &SubjectPublicKeyInfo { + self.into() } } -impl<'a> From<&'a PublicKey<'a>> for &'a SubjectPublicKeyInfo { - fn from(key: &'a PublicKey<'a>) -> Self { - key.0.as_ref() +impl AsRef for PublicKey { + #[inline] + fn as_ref(&self) -> &PublicKey { + self } } -impl From for OwnedPublicKey { - fn from(key: PrivateKey) -> Self { - Self(PublicKeyInner::Owned(key.into())) +impl PublicKey { + pub fn to_der(&self) -> Result, KeyError> { + picky_asn1_der::to_vec(&self.0).context(Asn1Serialization { + element: "subject public key info", + }) } -} -impl OwnedPublicKey { - pub fn as_borrowed(&self) -> PublicKey<'_> { - PublicKey(PublicKeyInner::Ref(self.0.as_ref())) + pub fn to_pem(&self) -> Result { + Ok(to_pem(PUBLIC_KEY_PEM_LABEL, &self.to_der()?)) } pub fn from_pem(pem: &Pem) -> Result { @@ -236,13 +225,11 @@ impl OwnedPublicKey { } pub fn from_der>(der: &T) -> Result { - Ok(Self( - picky_asn1_der::from_bytes::(der.as_ref()) - .context(Asn1Deserialization { - element: "subject public key info", - })? - .into(), - )) + Ok(Self(picky_asn1_der::from_bytes(der.as_ref()).context( + Asn1Deserialization { + element: "subject public key info", + }, + )?)) } pub fn from_rsa_der>(der: &T) -> Result { @@ -255,33 +242,14 @@ impl OwnedPublicKey { element: "rsa public key", })?; - Ok(Self( - SubjectPublicKeyInfo { - algorithm: AlgorithmIdentifier::new_rsa_encryption(), - subject_public_key: PublicKey::RSA(public_key.into()), - } - .into(), - )) - } -} - -impl<'a> PublicKey<'a> { - pub fn to_der(&self) -> Result, KeyError> { - picky_asn1_der::to_vec(self.0.as_ref()).context(Asn1Serialization { - element: "subject public key info", - }) - } - - pub fn to_pem(&self) -> Result { - Ok(to_pem(PUBLIC_KEY_PEM_LABEL, &self.to_der()?)) - } - - pub fn into_owned(self) -> OwnedPublicKey { - Self(PublicKeyInner::Owned(self.0.into_owned())) + Ok(Self(SubjectPublicKeyInfo { + algorithm: AlgorithmIdentifier::new_rsa_encryption(), + subject_public_key: PublicKey::RSA(public_key.into()), + })) } pub(crate) fn as_inner(&self) -> &SubjectPublicKeyInfo { - self.0.as_ref() + &self.0 } } diff --git a/picky/src/x509/certificate.rs b/picky/src/x509/certificate.rs index 932cfe3e..7f3c6da4 100644 --- a/picky/src/x509/certificate.rs +++ b/picky/src/x509/certificate.rs @@ -1,5 +1,5 @@ use crate::{ - key::{OwnedPublicKey, PrivateKey, PublicKey}, + key::{PrivateKey, PublicKey}, oids, pem::Pem, signature::{SignatureError, SignatureHashType}, @@ -246,7 +246,7 @@ impl Cert { (self.0.tbs_certificate.extensions.0).0.as_slice() } - pub fn public_key(&self) -> PublicKey { + pub fn public_key(&self) -> &PublicKey { (&self.0.tbs_certificate.subject_public_key_info).into() } @@ -389,7 +389,7 @@ enum SubjectInfos { Csr(Csr), NameAndPublicKey { name: DirectoryName, - public_key: OwnedPublicKey, + public_key: PublicKey, }, } @@ -447,7 +447,7 @@ impl<'a> CertificateBuilder<'a> { /// Required (alternatives: `subject_from_csr`, `self_signed`) #[inline] - pub fn subject(&self, subject_name: DirectoryName, public_key: OwnedPublicKey) -> &Self { + pub fn subject(&self, subject_name: DirectoryName, public_key: PublicKey) -> &Self { self.inner.borrow_mut().subject_infos = Some(SubjectInfos::NameAndPublicKey { name: subject_name, public_key, diff --git a/picky/src/x509/csr.rs b/picky/src/x509/csr.rs index a9c80860..69bdbb1e 100644 --- a/picky/src/x509/csr.rs +++ b/picky/src/x509/csr.rs @@ -1,5 +1,5 @@ use crate::{ - key::{OwnedPublicKey, PrivateKey, PublicKey}, + key::{PrivateKey, PublicKey}, pem::Pem, signature::{SignatureError, SignatureHashType}, x509::{ @@ -98,11 +98,11 @@ impl Csr { self.0.certification_request_info.subject.clone().into() } - pub fn public_key(&self) -> PublicKey { + pub fn public_key(&self) -> &PublicKey { (&self.0.certification_request_info.subject_public_key_info).into() } - pub fn into_subject_infos(self) -> (DirectoryName, OwnedPublicKey) { + pub fn into_subject_infos(self) -> (DirectoryName, PublicKey) { ( self.0.certification_request_info.subject.into(), self.0.certification_request_info.subject_public_key_info.into(), From 7d46477ef92f199a167c5865e7b3f6cfa745d61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Mon, 23 Dec 2019 11:54:10 -0500 Subject: [PATCH 4/5] Patch backtrace and libz-sys crates to make CI build on android/windows --- Cargo.lock | 24 +++++++----------------- Cargo.toml | 6 +++++- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cb95299..7553abce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,23 +74,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "backtrace" version = "0.3.40" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "git+https://github.com/Devolutions/backtrace-rs?branch=wayk#91667a922520f0916852a93e3c69346cfe0a2a2b" dependencies = [ - "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "backtrace-sys" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "base64" version = "0.9.3" @@ -401,7 +391,7 @@ dependencies = [ "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", "conan 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (git+https://github.com/Devolutions/libz-sys?branch=wayk)", "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -463,7 +453,7 @@ name = "failure" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.40 (git+https://github.com/Devolutions/backtrace-rs?branch=wayk)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -795,9 +785,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libz-sys" version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "git+https://github.com/Devolutions/libz-sys?branch=wayk#5cb7793a1be43572c9132ea8b433788a08aaffcc" dependencies = [ "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", + "conan 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2577,8 +2568,7 @@ dependencies = [ "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" -"checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" -"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" +"checksum backtrace 0.3.40 (git+https://github.com/Devolutions/backtrace-rs?branch=wayk)" = "" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" @@ -2661,7 +2651,7 @@ dependencies = [ "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" "checksum libm 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" "checksum libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" -"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" +"checksum libz-sys 1.0.25 (git+https://github.com/Devolutions/libz-sys?branch=wayk)" = "" "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e57b3997725d2b60dbec1297f6c2e2957cc383db1cebd6be812163f969c7d586" diff --git a/Cargo.toml b/Cargo.toml index 43bdefc0..3f6b0e3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,8 @@ members = [ "picky-server", "picky-asn1", "picky-asn1-der", -] \ No newline at end of file +] + +[patch.crates-io] +backtrace = { git = "https://github.com/Devolutions/backtrace-rs", branch = "wayk" } +libz-sys = { git = "https://github.com/Devolutions/libz-sys", branch = "wayk" } \ No newline at end of file From d7b2eb2f83166f5ccbba31d1375aa9819bd1b138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Mon, 23 Dec 2019 13:18:37 -0500 Subject: [PATCH 5/5] ci: make sure windows64 job build for x86_64 target --- ci/azure-pipelines.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/azure-pipelines.yaml b/ci/azure-pipelines.yaml index c8e53c50..04ae645e 100644 --- a/ci/azure-pipelines.yaml +++ b/ci/azure-pipelines.yaml @@ -53,9 +53,9 @@ jobs: - powershell: | conan install openssl/$(OPENSSL_VERSION)@devolutions/stable -g virtualenv -pr windows-x86_64 .\activate.ps1 - cargo build --release + cargo build --release --target=x86_64-pc-windows-msvc mkdir $(Build.ArtifactStagingDirectory)/windows/x86_64 - cp $(Build.Repository.LocalPath)/target/release/picky-server.exe $(Build.ArtifactStagingDirectory)/windows/x86_64/ + cp $(Build.Repository.LocalPath)/target/x86_64-pc-windows-msvc/release/picky-server.exe $(Build.ArtifactStagingDirectory)/windows/x86_64/ displayName: Building picky-rs env: RUSTFLAGS: '-C target-feature=+crt-static'