diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/pasetors/src | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/pasetors/src')
-rw-r--r-- | vendor/pasetors/src/claims.rs | 734 | ||||
-rw-r--r-- | vendor/pasetors/src/common.rs | 97 | ||||
-rw-r--r-- | vendor/pasetors/src/errors.rs | 76 | ||||
-rw-r--r-- | vendor/pasetors/src/footer.rs | 396 | ||||
-rw-r--r-- | vendor/pasetors/src/keys.rs | 144 | ||||
-rw-r--r-- | vendor/pasetors/src/lib.rs | 360 | ||||
-rw-r--r-- | vendor/pasetors/src/pae.rs | 78 | ||||
-rw-r--r-- | vendor/pasetors/src/paserk.rs | 860 | ||||
-rw-r--r-- | vendor/pasetors/src/serde.rs | 133 | ||||
-rw-r--r-- | vendor/pasetors/src/token.rs | 656 | ||||
-rw-r--r-- | vendor/pasetors/src/version.rs | 37 | ||||
-rw-r--r-- | vendor/pasetors/src/version2.rs | 828 | ||||
-rw-r--r-- | vendor/pasetors/src/version3.rs | 817 | ||||
-rw-r--r-- | vendor/pasetors/src/version4.rs | 983 |
14 files changed, 6199 insertions, 0 deletions
diff --git a/vendor/pasetors/src/claims.rs b/vendor/pasetors/src/claims.rs new file mode 100644 index 000000000..b6d97580a --- /dev/null +++ b/vendor/pasetors/src/claims.rs @@ -0,0 +1,734 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "std")))] + +use crate::errors::Error; +use serde_json::Value; +use std::collections::HashMap; +use time::format_description::well_known::Rfc3339; +use time::{Duration, OffsetDateTime}; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A collection of claims that are passed as payload for a PASETO token. +pub struct Claims { + list_of: HashMap<String, Value>, +} + +impl Claims { + /// Keys for registered claims, that are reserved for usage by PASETO in top-level. + pub const REGISTERED_CLAIMS: [&'static str; 7] = + ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"]; + + /// Create a new `Claims` instance, setting: + /// - `iat`, `nbf` to current UTC time + /// - `exp` to one hour + /// + /// Errors: + /// - If adding current time with one hour would overflow + pub fn new() -> Result<Self, Error> { + let iat = OffsetDateTime::now_utc(); + let nbf = iat; + let mut exp = iat; + exp += Duration::hours(1); + + let mut claims = Self { + list_of: HashMap::new(), + }; + + claims.issued_at(&iat.format(&Rfc3339).map_err(|_| Error::InvalidClaim)?)?; + claims.not_before(&nbf.format(&Rfc3339).map_err(|_| Error::InvalidClaim)?)?; + claims.expiration(&exp.format(&Rfc3339).map_err(|_| Error::InvalidClaim)?)?; + + Ok(claims) + } + + /// Removes the `exp` claim, indicating a token that never expires. + pub fn non_expiring(&mut self) { + if self.contains_claim("exp") { + self.list_of.remove_entry("exp").unwrap(); + } + } + + /// Add additional claims. If `claim` already exists, it is replaced with the new. + /// + /// Errors: + /// - `claim` is a reserved claim (see [`Self::REGISTERED_CLAIMS`]) + pub fn add_additional(&mut self, claim: &str, value: impl Into<Value>) -> Result<(), Error> { + if !Self::REGISTERED_CLAIMS.contains(&claim) { + self.list_of.insert(claim.into(), value.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Checks whether a specific claim has been added to the list. + /// + /// E.g `contains_claim("iss") == true` if `iss` has been added before. + pub fn contains_claim(&self, claim: &str) -> bool { + self.list_of.contains_key(claim) + } + + /// Return Some(claim value) if claims list contains the `claim`. + /// None otherwise. + pub fn get_claim(&self, claim: &str) -> Option<&Value> { + self.list_of.get(claim) + } + + /// Set the `iss` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `iss` is empty + pub fn issuer(&mut self, iss: &str) -> Result<(), Error> { + if !iss.is_empty() { + self.list_of.insert("iss".into(), iss.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `sub` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `sub` is empty + pub fn subject(&mut self, sub: &str) -> Result<(), Error> { + if !sub.is_empty() { + self.list_of.insert("sub".into(), sub.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `aud` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `aud` is empty + pub fn audience(&mut self, aud: &str) -> Result<(), Error> { + if !aud.is_empty() { + self.list_of.insert("aud".into(), aud.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `exp` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `exp` is empty + /// - `exp` cannot be parsed as a ISO 8601 compliant DateTime string. + pub fn expiration(&mut self, exp: &str) -> Result<(), Error> { + if let Ok(_exp_str) = OffsetDateTime::parse(exp, &Rfc3339) { + self.list_of.insert("exp".into(), exp.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `nbf` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `nbf` is empty + /// - `nbf` cannot be parsed as a ISO 8601 compliant DateTime string. + pub fn not_before(&mut self, nbf: &str) -> Result<(), Error> { + if let Ok(_nbf_str) = OffsetDateTime::parse(nbf, &Rfc3339) { + self.list_of.insert("nbf".into(), nbf.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `iat` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `iat` is empty + /// - `iat` cannot be parsed as a ISO 8601 compliant DateTime string. + pub fn issued_at(&mut self, iat: &str) -> Result<(), Error> { + if let Ok(_iat_str) = OffsetDateTime::parse(iat, &Rfc3339) { + self.list_of.insert("iat".into(), iat.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Set the `jti` claim. If it already exists, replace it with the new. + /// + /// Errors: + /// - `jti` is empty + pub fn token_identifier(&mut self, jti: &str) -> Result<(), Error> { + if !jti.is_empty() { + self.list_of.insert("jti".into(), jti.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Attempt to create `Claims` from a sequence of bytes. + /// + /// Errors: + /// - `bytes` contains non-UTF-8 sequences + /// - `bytes` does not decode as valid JSON + /// - `bytes` top-most JSON object does not decode to a map + /// - if any registered claims exist and they are not a `String` + /// - if `exp`, `nbf` or `iat` exist and they cannot be parsed as `DateTime` + pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> { + let input = bytes.to_vec(); + + Self::from_string(&String::from_utf8(input).map_err(|_| Error::ClaimInvalidUtf8)?) + } + + /// Attempt to create `Claims` from a string. + /// + /// Errors: + /// - `string` does not decode as valid JSON + /// - `string` top-most JSON object does not decode to a map + /// - if any registered claims exist and they are not a `String` + /// - if `exp`, `nbf` or `iat` exist and they cannot be parsed as `DateTime` + pub fn from_string(string: &str) -> Result<Self, Error> { + let list_of: HashMap<String, Value> = + serde_json::from_str(string).map_err(|_| Error::ClaimInvalidJson)?; + + // Validate any possible registered claims for their type + for registered_claim in Self::REGISTERED_CLAIMS { + if let Some(claim) = list_of.get(registered_claim) { + if let Some(claim_value) = claim.as_str() { + if registered_claim == "exp" + || registered_claim == "nbf" + || registered_claim == "iat" + { + OffsetDateTime::parse(claim_value, &Rfc3339) + .map_err(|_| Error::InvalidClaim)?; + } + } else { + return Err(Error::InvalidClaim); + } + } + } + + Ok(Self { list_of }) + } + + /// Return the JSON serialized representation of `Self`. + /// + /// Errors: + /// - `self` cannot be serialized as JSON + pub fn to_string(&self) -> Result<String, Error> { + match serde_json::to_string(&self.list_of) { + Ok(ret) => Ok(ret), + Err(_) => Err(Error::ClaimInvalidJson), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// The validation rules that are used to validate a set of [`Claims`]. +pub struct ClaimsValidationRules { + validate_currently_valid: bool, + allow_non_expiring: bool, + validate_issuer: Option<String>, + validate_subject: Option<String>, + validate_audience: Option<String>, + validate_token_identifier: Option<String>, +} + +impl Default for ClaimsValidationRules { + fn default() -> Self { + Self::new() + } +} + +impl ClaimsValidationRules { + /// Create a new `ClaimsValidationRules` instance, setting: + /// - validation of `iat`, `nbf`, `exp` true + pub fn new() -> Self { + Self { + validate_currently_valid: true, + allow_non_expiring: false, + validate_issuer: None, + validate_subject: None, + validate_audience: None, + validate_token_identifier: None, + } + } + + /// Explicitly allow non-expiring tokens (i.e. the `exp` claim is missing). + pub fn allow_non_expiring(&mut self) { + self.allow_non_expiring = true; + } + + /// Set the `valid_issuer` the claims should be validated against. + pub fn validate_issuer_with(&mut self, valid_issuer: &str) { + self.validate_issuer = Some(valid_issuer.to_string()); + } + + /// Set the `valid_subject` the claims should be validated against. + pub fn validate_subject_with(&mut self, valid_subject: &str) { + self.validate_subject = Some(valid_subject.to_string()); + } + + /// Set the `valid_audience` the claims should be validated against. + pub fn validate_audience_with(&mut self, valid_audience: &str) { + self.validate_audience = Some(valid_audience.to_string()); + } + + /// Set the `valid_token_identifier` the claims should be validated against. + pub fn validate_token_identifier_with(&mut self, valid_token_identifier: &str) { + self.validate_token_identifier = Some(valid_token_identifier.to_string()); + } + + /// Validate the set of registered `claims` against the currently defined validation rules. + /// + /// If `claims` has defined the `exp` claim, this is validated regardless of whether the rules + /// have allowed for non-expiring. Non-expiring means that there should be no `exp` in `claims`. + /// + /// Errors: + /// - Token is expired + /// - Token is not yet valid + /// - Token was issued in the future + /// - Token has no `exp` claim but the validation rules do not allow non-expiring tokens + /// - The claims values cannot be converted to `str` + /// - `iat`, `nbf` and `exp` fail `str -> DateTime` conversion + /// - Claim `iss`, `sub`, `aud`, `jti` does not match the expected + /// - `claims` has no `nbf` or `iat` + /// - a claim was registered for validation in the rules but is missing from the actual `claims` + /// + /// NOTE: This __does not__ validate any non-registered claims (see [`Claims::REGISTERED_CLAIMS`]). They must be validated + /// separately. + pub fn validate_claims(&self, claims: &Claims) -> Result<(), Error> { + if self.validate_currently_valid { + match (claims.list_of.get("iat"), claims.list_of.get("nbf")) { + (Some(iat), Some(nbf)) => match (iat.as_str(), nbf.as_str()) { + (Some(iat), Some(nbf)) => { + let iat = OffsetDateTime::parse(iat, &Rfc3339) + .map_err(|_| Error::ClaimValidation)?; + let nbf = OffsetDateTime::parse(nbf, &Rfc3339) + .map_err(|_| Error::ClaimValidation)?; + let current_time = OffsetDateTime::now_utc(); + + if current_time < nbf || current_time < iat { + return Err(Error::ClaimValidation); + } + } + _ => return Err(Error::ClaimValidation), + }, + _ => return Err(Error::ClaimValidation), + } + } + + if let Some(exp) = claims.list_of.get("exp") { + if let Some(exp) = exp.as_str() { + let exp = + OffsetDateTime::parse(exp, &Rfc3339).map_err(|_| Error::ClaimValidation)?; + let current_time = OffsetDateTime::now_utc(); + + if current_time > exp { + return Err(Error::ClaimValidation); + } + } else { + return Err(Error::ClaimValidation); + } + } else if !self.allow_non_expiring { + // We didn't explicitly allow non-expiring tokens so we expect `exp` claim. + return Err(Error::ClaimValidation); + } + + if let Some(expected_issuer) = &self.validate_issuer { + if let Some(actual_issuer) = claims.list_of.get("iss") { + if expected_issuer != actual_issuer { + return Err(Error::ClaimValidation); + } + } else { + return Err(Error::ClaimValidation); + } + } + + if let Some(expected_subject) = &self.validate_subject { + if let Some(actual_subject) = claims.list_of.get("sub") { + if expected_subject != actual_subject { + return Err(Error::ClaimValidation); + } + } else { + return Err(Error::ClaimValidation); + } + } + + if let Some(expected_audience) = &self.validate_audience { + if let Some(actual_audience) = claims.list_of.get("aud") { + if expected_audience != actual_audience { + return Err(Error::ClaimValidation); + } + } else { + return Err(Error::ClaimValidation); + } + } + + if let Some(expected_token_identifier) = &self.validate_token_identifier { + if let Some(actual_token_identifier) = claims.list_of.get("jti") { + if expected_token_identifier != actual_token_identifier { + return Err(Error::ClaimValidation); + } + } else { + return Err(Error::ClaimValidation); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_empty_claim_value() { + let mut claims = Claims::new().unwrap(); + + assert!(claims.issuer("").is_err()); + assert!(claims.subject("").is_err()); + assert!(claims.audience("").is_err()); + assert!(claims.expiration("").is_err()); + assert!(claims.not_before("").is_err()); + assert!(claims.issued_at("").is_err()); + assert!(claims.token_identifier("").is_err()); + } + + #[test] + fn test_error_on_arbitrary_registered() { + let mut claims = Claims::new().unwrap(); + + assert!(claims.add_additional("iss", "test").is_err()); + assert!(claims.add_additional("sub", "test").is_err()); + assert!(claims.add_additional("aud", "test").is_err()); + assert!(claims + .add_additional("exp", "2014-11-28T21:00:09+09:00") + .is_err()); + assert!(claims + .add_additional("nbf", "2014-11-28T21:00:09+09:00") + .is_err()); + assert!(claims + .add_additional("iat", "2014-11-28T21:00:09+09:00") + .is_err()); + assert!(claims.add_additional("jti", "test").is_err()); + + assert!(claims.add_additional("not_reserved", "test").is_ok()); + } + + #[test] + fn test_failed_datetime_parsing() { + let mut claims = Claims::new().unwrap(); + + assert!(claims + .expiration("this is not a ISO 8601 DateTime string") + .is_err()); + assert!(claims + .not_before("this is not a ISO 8601 DateTime string") + .is_err()); + assert!(claims + .issued_at("this is not a ISO 8601 DateTime string") + .is_err()); + + claims.list_of.insert( + "iat".to_string(), + "this is not a ISO 8601 DateTime string".into(), + ); + claims.list_of.insert( + "nbf".to_string(), + "this is not a ISO 8601 DateTime string".into(), + ); + + let validation_rules = ClaimsValidationRules::default(); + assert!(validation_rules.validate_claims(&claims).is_err()); + } + + #[test] + fn test_contains_claim() { + let mut claims = Claims::new().unwrap(); + + // Default claims + assert!(claims.contains_claim("iat")); + assert!(claims.contains_claim("nbf")); + assert!(claims.contains_claim("exp")); + + assert!(!claims.contains_claim("iss")); + claims.issuer("testIssuer").unwrap(); + assert!(claims.contains_claim("iss")); + + assert!(!claims.contains_claim("aud")); + claims.audience("testAudience").unwrap(); + assert!(claims.contains_claim("aud")); + } + + #[test] + fn test_basic_claims_validation() { + // Set all claims plus a custom one + let mut claims = Claims::new().unwrap(); + claims.issuer("testIssuer").unwrap(); + claims.audience("testAudience").unwrap(); + claims.subject("testSubject").unwrap(); + claims.token_identifier("testIdentifier").unwrap(); + claims.add_additional("testClaim", "testValue").unwrap(); + + let mut claims_validation = ClaimsValidationRules::new(); + claims_validation.validate_issuer_with("testIssuer"); + claims_validation.validate_audience_with("testAudience"); + claims_validation.validate_subject_with("testSubject"); + claims_validation.validate_token_identifier_with("testIdentifier"); + + assert!(&claims_validation.validate_claims(&claims).is_ok()); + + // Mismatch between Claims `iss` and ClaimValidationRules `iss` + claims_validation.validate_issuer_with("testIssuerFalse"); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims_validation.validate_issuer_with("testIssuer"); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims + .list_of + .insert("iss".to_string(), "testIssuerFalse".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("iss".to_string(), "testIssuer".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims.list_of.remove_entry("iss").unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("iss".to_string(), "testIssuer".into()); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + + // Mismatch between Claims `aud` and ClaimValidationRules `aud` + claims_validation.validate_audience_with("testAudienceFalse"); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims_validation.validate_audience_with("testAudience"); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims + .list_of + .insert("aud".to_string(), "testAudienceFalse".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("aud".to_string(), "testAudience".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims.list_of.remove_entry("aud").unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("aud".to_string(), "testAudience".into()); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + + // Mismatch between Claims `sub` and ClaimValidationRules `sub` + claims_validation.validate_subject_with("testSubjectFalse"); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims_validation.validate_subject_with("testSubject"); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims + .list_of + .insert("sub".to_string(), "testSubjectFalse".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("sub".to_string(), "testSubject".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims.list_of.remove_entry("sub").unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("sub".to_string(), "testSubject".into()); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + + // Mismatch between Claims `jti` and ClaimValidationRules `jti` + claims_validation.validate_token_identifier_with("testIdentifierFalse"); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims_validation.validate_token_identifier_with("testIdentifier"); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims + .list_of + .insert("jti".to_string(), "testIdentifierFalse".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("jti".to_string(), "testIdentifier".into()) + .unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + claims.list_of.remove_entry("jti").unwrap(); + assert!(&claims_validation.validate_claims(&claims).is_err()); + claims + .list_of + .insert("jti".to_string(), "testIdentifier".into()); + assert!(&claims_validation.validate_claims(&claims).is_ok()); + } + + #[test] + fn test_invalid_token_at_time() { + let claims = Claims::new().unwrap(); + let claims_validation = ClaimsValidationRules::new(); + + assert!(claims_validation.validate_claims(&claims).is_ok()); + + // Outdated + let mut outdated_claims = claims.clone(); + outdated_claims + .list_of + .insert("iat".to_string(), "2019-01-01T00:00:00+00:00".into()) + .unwrap(); + assert!(claims_validation.validate_claims(&outdated_claims).is_ok()); + outdated_claims + .list_of + .insert("nbf".to_string(), "2019-01-01T00:00:00+00:00".into()) + .unwrap(); + assert!(claims_validation.validate_claims(&outdated_claims).is_ok()); + outdated_claims + .list_of + .insert("exp".to_string(), "2019-01-01T00:00:00+00:00".into()) + .unwrap(); + // Expired + assert_eq!( + claims_validation + .validate_claims(&outdated_claims) + .unwrap_err(), + Error::ClaimValidation + ); + outdated_claims.non_expiring(); + let mut claims_validation_allow_expiry = claims_validation.clone(); + // Rules not yet defined to allow non-expiring + assert!(claims_validation_allow_expiry + .validate_claims(&outdated_claims) + .is_err()); + claims_validation_allow_expiry.allow_non_expiring(); + // Test if claim has `exp` but rules dictate allowing non-expiring (which is ignored + // as long as `exp` is present in the claims) so it's still expired + outdated_claims + .expiration("2019-01-01T00:00:00+00:00") + .unwrap(); + assert!(claims_validation_allow_expiry + .validate_claims(&outdated_claims) + .is_err()); + // Missing `exp` and allow in rules match + outdated_claims.non_expiring(); + assert!(claims_validation_allow_expiry + .validate_claims(&outdated_claims) + .is_ok()); + + // In-future + let mut future_claims = claims.clone(); + let old_iat = future_claims + .list_of + .insert("iat".to_string(), "2028-01-01T00:00:00+00:00".into()) + .unwrap(); + // Issued in future + assert_eq!( + claims_validation + .validate_claims(&future_claims) + .unwrap_err(), + Error::ClaimValidation + ); + future_claims.issued_at(old_iat.as_str().unwrap()).unwrap(); + assert!(claims_validation.validate_claims(&future_claims).is_ok()); + // Not yet valid + let old_nbf = future_claims + .list_of + .insert("nbf".to_string(), "2028-01-01T00:00:00+00:00".into()) + .unwrap(); + assert_eq!( + claims_validation + .validate_claims(&future_claims) + .unwrap_err(), + Error::ClaimValidation + ); + future_claims.not_before(old_nbf.as_str().unwrap()).unwrap(); + assert!(claims_validation.validate_claims(&future_claims).is_ok()); + + // We expect `iat`, `exp` and `nbf` if we validate time + let mut incomplete_claims = claims.clone(); + incomplete_claims.list_of.remove_entry("iat").unwrap(); + assert_eq!( + claims_validation + .validate_claims(&incomplete_claims) + .unwrap_err(), + Error::ClaimValidation + ); + + let mut incomplete_claims = claims.clone(); + incomplete_claims.list_of.remove_entry("exp").unwrap(); + assert_eq!( + claims_validation + .validate_claims(&incomplete_claims) + .unwrap_err(), + Error::ClaimValidation + ); + + let mut incomplete_claims = claims; + incomplete_claims.list_of.remove_entry("nbf").unwrap(); + assert_eq!( + claims_validation + .validate_claims(&incomplete_claims) + .unwrap_err(), + Error::ClaimValidation + ); + } + + #[test] + fn test_add_non_string_additional_claims() { + // Set all claims plus a custom one + let mut claims = Claims::new().unwrap(); + + let add_claims_one = vec!["a", "b", "b"]; + let add_claims_two = 32; + let add_claims_three = true; + + claims.add_additional("one", add_claims_one).unwrap(); + claims.add_additional("two", add_claims_two).unwrap(); + claims.add_additional("three", add_claims_three).unwrap(); + + let as_string = claims.to_string().unwrap(); + let from_converted = Claims::from_string(&as_string).unwrap(); + assert_eq!(from_converted, claims); + + assert!(claims.contains_claim("one")); + assert!(claims.contains_claim("two")); + assert!(claims.contains_claim("three")); + } + + #[test] + fn test_token_no_expiration() { + let mut claims = Claims::new().unwrap(); + let mut claims_validation = ClaimsValidationRules::new(); + + claims.non_expiring(); + // Claims validation is not explicitly set to allow non-expiring, so we get error here + // because claims is missing exp + assert!(claims_validation.validate_claims(&claims).is_err()); + claims_validation.allow_non_expiring(); + assert!(claims_validation.validate_claims(&claims).is_ok()); + } + + #[test] + fn test_token_missing_iat_nbf_exp() { + let claims_validation = ClaimsValidationRules::new(); + + // Default validation rules validate these times but error if they're missing + let mut claims = Claims::new().unwrap(); + claims.list_of.remove("iat"); + assert!(claims_validation.validate_claims(&claims).is_err()); + + let mut claims = Claims::new().unwrap(); + claims.list_of.remove("nbf"); + assert!(claims_validation.validate_claims(&claims).is_err()); + + let mut claims = Claims::new().unwrap(); + claims.list_of.remove("exp"); + assert!(claims_validation.validate_claims(&claims).is_err()); + } +} diff --git a/vendor/pasetors/src/common.rs b/vendor/pasetors/src/common.rs new file mode 100644 index 000000000..b95583f62 --- /dev/null +++ b/vendor/pasetors/src/common.rs @@ -0,0 +1,97 @@ +use crate::errors::Error; +use crate::token::private::Purpose; +use crate::token::UntrustedToken; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use ct_codecs::{Base64UrlSafeNoPadding, Decoder, Encoder}; +use subtle::ConstantTimeEq; + +/// Encode bytes with Base64 URL-safe and no padding. +pub(crate) fn encode_b64<T: AsRef<[u8]>>(bytes: T) -> Result<String, Error> { + let inlen = bytes.as_ref().len(); + let mut buf = vec![0u8; Base64UrlSafeNoPadding::encoded_len(inlen)?]; + + let ret: String = Base64UrlSafeNoPadding::encode_to_str(&mut buf, bytes)?.into(); + + Ok(ret) +} + +/// Decode string with Base64 URL-safe and no padding. +pub(crate) fn decode_b64<T: AsRef<[u8]>>(encoded: T) -> Result<Vec<u8>, Error> { + let inlen = encoded.as_ref().len(); + // We can use encoded len here, even if it returns more than needed, + // because ct-codecs allows this. + let mut buf = vec![0u8; Base64UrlSafeNoPadding::encoded_len(inlen)?]; + + let ret: Vec<u8> = Base64UrlSafeNoPadding::decode(&mut buf, encoded, None)?.into(); + + Ok(ret) +} + +/// If a footer is present, this is validated against the supplied. +pub(crate) fn validate_footer_untrusted_token<T: Purpose<V>, V: Version>( + token: &UntrustedToken<T, V>, + footer: Option<&[u8]>, +) -> Result<(), Error> { + // A known footer was supplied for comparison. + if let Some(known_footer) = footer { + if token.untrusted_footer().is_empty() { + // If one was supplied, one must exist in the untrusted. + return Err(Error::TokenValidation); + } + + if !bool::from(known_footer.ct_eq(token.untrusted_footer())) { + return Err(Error::TokenValidation); + } + } + + Ok(()) +} + +#[cfg(test)] +pub(crate) mod tests { + use alloc::string::String; + use alloc::vec::Vec; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct TestFile { + pub(crate) name: String, + pub(crate) tests: Vec<PasetoTest>, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct PasetoTest { + pub(crate) name: String, + #[serde(rename(deserialize = "expect-fail"))] + pub(crate) expect_fail: bool, + pub(crate) key: Option<String>, + pub(crate) nonce: Option<String>, + #[serde(rename(deserialize = "public-key"))] + pub(crate) public_key: Option<String>, + #[serde(rename(deserialize = "secret-key"))] + pub(crate) secret_key: Option<String>, + #[serde(rename(deserialize = "secret-key-seed"))] + pub(crate) secret_key_seed: Option<String>, + #[serde(rename(deserialize = "public-key-pem"))] + pub(crate) public_key_pem: Option<String>, + #[serde(rename(deserialize = "secret-key-pem"))] + pub(crate) secret_key_pem: Option<String>, + pub(crate) token: String, + pub(crate) payload: Option<Value>, + pub(crate) footer: String, + #[serde(rename(deserialize = "implicit-assertion"))] + pub(crate) implicit_assertion: String, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct Payload { + pub(crate) data: String, + pub(crate) exp: String, + } +} diff --git a/vendor/pasetors/src/errors.rs b/vendor/pasetors/src/errors.rs new file mode 100644 index 000000000..f970ff334 --- /dev/null +++ b/vendor/pasetors/src/errors.rs @@ -0,0 +1,76 @@ +#[derive(Debug, PartialEq, Eq)] +/// Errors for token operations. +pub enum Error { + /// Error for a token with an invalid format. + TokenFormat, + /// Error for a failed Base64 (URL-safe without padding) encoding/decoding. + Base64, + /// Error for a failed token validation. + TokenValidation, + /// Error for an invalid key. + Key, + /// Error for a failed encryption operation. + Encryption, + /// Error for a failed attempt to generate bytes using a CSPRNG. + Csprng, + /// Error for a conversion that would be lossy. + LossyConversion, + /// Error for attempting to create a token with an empty payload. + EmptyPayload, + /// Error for attempting to create an invalid claim. + InvalidClaim, + /// Claim validation error. See [`crate::claims::ClaimsValidationRules::validate_claims`]. + ClaimValidation, + /// Error for attempting to parse a Claim but found invalid UTF-8 sequence. + ClaimInvalidUtf8, + /// Error for attempting to parse a Claim but found invalid JSON sequence. + ClaimInvalidJson, + /// Error during (de)serialization of PASERK types. + PaserkParsing, + /// Error during signing of a message. + Signing, + /// Error during conversion between uncompressed<->compressed public keys. + PublicKeyConversion, + /// Error during key generation. + KeyGeneration, + /// The payload was not valid UTF-8. + PayloadInvalidUtf8, + /// Error during parsing of a `Footer`. + FooterParsing, +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +#[cfg(feature = "std")] +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:?}", self)) + } +} + +impl From<ct_codecs::Error> for Error { + fn from(_: ct_codecs::Error) -> Self { + Error::Base64 + } +} + +impl From<getrandom::Error> for Error { + fn from(_: getrandom::Error) -> Self { + Error::Csprng + } +} + +impl From<core::num::TryFromIntError> for Error { + fn from(_: core::num::TryFromIntError) -> Self { + Error::LossyConversion + } +} + +#[test] +fn test_error_from_impls() { + let _ = format!("{:?}", Error::TokenFormat); + let _ = format!("{}", Error::TokenFormat); + assert_eq!(Error::from(ct_codecs::Error::InvalidInput), Error::Base64); + assert_eq!(Error::from(getrandom::Error::FAILED_RDRAND), Error::Csprng); +} diff --git a/vendor/pasetors/src/footer.rs b/vendor/pasetors/src/footer.rs new file mode 100644 index 000000000..cff1998df --- /dev/null +++ b/vendor/pasetors/src/footer.rs @@ -0,0 +1,396 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "std")))] + +use crate::errors::Error; +#[cfg(feature = "paserk")] +use crate::paserk::{FormatAsPaserk, Id}; +use regex::Regex; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A footer with optional claims that are JSON-encoded. +pub struct Footer { + list_of: HashMap<String, Value>, + max_keys: usize, + max_len: usize, +} + +impl Default for Footer { + fn default() -> Self { + Self::new() + } +} + +impl Footer { + /// Keys for registered claims in the footer, that are reserved for usage by PASETO in top-level. + pub const REGISTERED_CLAIMS: [&'static str; 2] = ["kid", "wpk"]; + + /// All PASERK types that are (implemented in this library) unsafe in the footer. + pub const DISALLOWED_FOOTER: [&'static str; 8] = [ + "k2.local.", + "k4.local.", + "k2.secret.", + "k3.secret.", + "k4.secret.", + "k2.public.", + "k3.public.", + "k4.public.", + ]; + + /// See [PASETO docs] for the reason behind this limit. + /// + /// Maximum number of named keys within an object. + /// + /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string + pub const DEFAULT_MAX_KEYS: usize = 512; + + /// See [PASETO docs] for the reason behind this limit. + /// + /// Maximum length of the JSON-encoded string. + /// + /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string + pub const DEFAULT_MAX_LEN: usize = 8192; + + /// See [PASETO docs] for the reason behind this limit. + /// + /// This value has been set by `serde_json` and cannot be changed. + /// + /// [PASETO docs]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string + pub const MAX_RECURSION_DEPTH: usize = 128; + + /// Create a new `Footer` instance. + pub fn new() -> Self { + Self { + list_of: HashMap::new(), + max_keys: Self::DEFAULT_MAX_KEYS, + max_len: Self::DEFAULT_MAX_LEN, + } + } + + /// Change the default (512) amount of maximum number of named keys within an object. + /// + /// __NOTE__: There should be no need to change this if you don't know this is a specific problem for you. + pub fn max_keys(&mut self, max_keys: usize) { + self.max_keys = max_keys; + } + + /// Change the default (8192) amount of maximum number of named keys within an object. + /// + /// __NOTE__: There should be no need to change this if you don't know this is a specific problem for you. + pub fn max_len(&mut self, max_len: usize) { + self.max_len = max_len; + } + + /// Add additional claims. If `claim` already exists, it is replaced with the new. + /// + /// Errors: + /// - `claim` is a reserved claim (see [`Self::REGISTERED_CLAIMS`]) + /// - `value` is any of (starts with) the disallowed PASERK types (see [`Self::DISALLOWED_FOOTER`]). + pub fn add_additional(&mut self, claim: &str, value: &str) -> Result<(), Error> { + for unsafe_value in Self::DISALLOWED_FOOTER { + if value.starts_with(unsafe_value) { + return Err(Error::InvalidClaim); + } + } + + if !Self::REGISTERED_CLAIMS.contains(&claim) { + self.list_of.insert(claim.into(), value.into()); + Ok(()) + } else { + Err(Error::InvalidClaim) + } + } + + /// Checks whether a specific claim has been added to the list. + /// + /// E.g `contains_claim("kid") == true` if `kid` has been added before. + pub fn contains_claim(&self, claim: &str) -> bool { + self.list_of.contains_key(claim) + } + + /// Return Some(claim value) if claims list contains the `claim`. + /// None otherwise. + pub fn get_claim(&self, claim: &str) -> Option<&Value> { + self.list_of.get(claim) + } + + #[cfg(feature = "paserk")] + /// Set the `kid` claim. If it already exists, replace it with the new. + pub fn key_id(&mut self, id: &Id) { + let mut paserk_kid = String::new(); + id.fmt(&mut paserk_kid).unwrap(); + + self.list_of.insert("kid".into(), paserk_kid.into()); + } + + /// Attempt to create `Footer` from a sequence of bytes. + /// + /// Errors: + /// - `bytes` contains non-UTF-8 sequences + /// - `bytes` does not decode as valid JSON + /// - `bytes` top-most JSON object does not decode to a map + /// - if any registered claims exist and they are not a `String` + /// - Parsing JSON maps and arrays that are more than 128 layers deep + /// - Maximum number of named keys is exceeded + /// - Maximum JSON-encoded string length is exceeded + pub fn parse_bytes(&mut self, bytes: &[u8]) -> Result<(), Error> { + let input = bytes.to_vec(); + + self.parse_string(&String::from_utf8(input).map_err(|_| Error::FooterParsing)?) + } + + /// Attempt to parse a `Footer` from a string. + /// + /// Errors: + /// - `string` does not decode as valid JSON + /// - `string` top-most JSON object does not decode to a map + /// - if any registered claims exist and they are not a `String` + /// - Parsing JSON maps and arrays that are more than 128 layers deep + /// - Maximum number of named keys is exceeded + /// - Maximum JSON-encoded string length is exceeded + pub fn parse_string(&mut self, string: &str) -> Result<(), Error> { + if string.len() > self.max_len { + return Err(Error::FooterParsing); + } + if Regex::new(r#"[^\\]":"#).unwrap().find_iter(string).count() > self.max_keys { + return Err(Error::FooterParsing); + } + + self.list_of = serde_json::from_str(string).map_err(|_| Error::FooterParsing)?; + + Ok(()) + } + + /// Return the JSON serialized representation of `Self`. + /// + /// Errors: + /// - `self` cannot be serialized as JSON + pub fn to_string(&self) -> Result<String, Error> { + match serde_json::to_string(&self.list_of) { + Ok(ret) => Ok(ret), + Err(_) => Err(Error::FooterParsing), + } + } +} + +#[cfg(test)] +mod tests { + use crate::footer::Footer; + use regex::Regex; + + #[test] + fn test_count_keys() { + // https://www.rustescaper.com/ + let string = r#""name": "3-S-2", + "expect-fail": false, + "public-key": "02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb", + "secret-key": "20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96", + "secret-key-pem": "-----BEGIN EC PRIVATE KEY-----nMIGkAgEBBDAgNHYJYHR3rKj7+8XmIYRV8xmWaXku+LRm+qh73Gd5gUTISN0DZh7tnWsYkYTQM6pagBwYFK4EEACKhZANiAAT7y3xp7hxgV5vnozQTSHjZxcW/NdVS2rY8nAUA5ftFM72N9dyCSXERpnqMOcodMcvt8kgcrB8KcKee0HU23E79/s4CvEs8hBfnjnSUd/gcAm08EjSIz06iWjrNy4NakxR3I=n-----END EC PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+8t8ae4cYFeb56M0E0h42cXFvzXVUtq2nPAFAOX7RTO9jfXcgklxEaZ6jDnKHTHL7fJIHKwfCnCnntB1NtxO/f7OArxLPIQX5n40lHf4HAJtPBI0iM9Oolo6zcuDWpMUdyn-----END PUBLIC KEY-----", + "token": "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9", + "payload": "{"data":"this is a signed message","exp":"2022-01-01T00:00:00+00:00"}", + "footer": "{"kid":"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn"}", + "implicit-assertion": """#; + + assert_eq!( + Regex::new(r#"[^\\]":"#).unwrap().find_iter(string).count(), + 13 + ); + } + + #[test] + fn err_on_max_keys() { + let mut footer = Footer::default(); + for n in 1..=11 { + footer + .add_additional(format!("{}", n).as_str(), "test") + .unwrap(); + } + + let mut footer_parse = Footer::default(); + footer_parse.max_keys(10); + assert!(footer_parse + .parse_bytes(footer.to_string().unwrap().as_bytes()) + .is_err()); + } + + #[test] + fn err_on_max_len() { + let mut footer = Footer::new(); + for n in 1..=11 { + footer + .add_additional(format!("{}", n).as_str(), "test") + .unwrap(); + } + let ser_footer = footer.to_string().unwrap(); + + let mut footer_parse = Footer::new(); + footer_parse.max_len(ser_footer.len() - 1); + assert!(footer_parse.parse_bytes(ser_footer.as_bytes()).is_err()); + } + + #[test] + fn err_on_custom_with_registered() { + let mut footer = Footer::new(); + + assert!(footer.add_additional("wpk", "test").is_err()); + assert!(footer.add_additional("kid", "test").is_err()); + assert!(footer.add_additional("custom", "test").is_ok()); + } + + #[test] + #[cfg(all(feature = "paserk", feature = "v2", feature = "v3", feature = "v4"))] + fn err_on_disallowed_in_footer() { + use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; + use crate::paserk::FormatAsPaserk; + use crate::version2::V2; + use crate::version3::V3; + use crate::version4::V4; + + let mut footer = Footer::new(); + + let kpv2 = AsymmetricKeyPair::<V2>::generate().unwrap(); + let kpv3 = AsymmetricKeyPair::<V3>::generate().unwrap(); + let kpv4 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let skv2 = SymmetricKey::<V2>::generate().unwrap(); + let skv4 = SymmetricKey::<V4>::generate().unwrap(); + + let mut buf = String::new(); + kpv2.secret.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + kpv2.public.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + kpv3.secret.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + kpv3.public.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + kpv4.secret.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + kpv4.public.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + skv2.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + + let mut buf = String::new(); + skv4.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("wpk", &buf).is_err()); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_err()); + } + + #[test] + #[cfg(all(feature = "paserk", feature = "v2", feature = "v3", feature = "v4"))] + fn kid_in_footer() { + use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; + use crate::paserk::{FormatAsPaserk, Id}; + use crate::version2::V2; + use crate::version3::V3; + use crate::version4::V4; + + let mut footer = Footer::new(); + + let kpv2 = AsymmetricKeyPair::<V2>::generate().unwrap(); + let kpv3 = AsymmetricKeyPair::<V3>::generate().unwrap(); + let kpv4 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let skv2 = SymmetricKey::<V2>::generate().unwrap(); + let skv4 = SymmetricKey::<V4>::generate().unwrap(); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv2.secret); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv2.public); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv3.secret); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv3.public); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv4.secret); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&kpv4.public); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&skv2); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + + let mut buf = String::new(); + let paserk_id = Id::from(&skv4); + paserk_id.fmt(&mut buf).unwrap(); + assert!(footer.add_additional("kid", &buf).is_err()); + assert!(footer.add_additional("custom", &buf).is_ok()); + footer.key_id(&paserk_id); + assert!(footer.contains_claim("kid")); + assert_eq!(footer.get_claim("kid").unwrap().as_str().unwrap(), buf); + } +} diff --git a/vendor/pasetors/src/keys.rs b/vendor/pasetors/src/keys.rs new file mode 100644 index 000000000..3c5c9bb58 --- /dev/null +++ b/vendor/pasetors/src/keys.rs @@ -0,0 +1,144 @@ +use crate::errors::Error; +use crate::version::private::Version; +use alloc::vec::Vec; +use core::fmt::Debug; +use core::marker::PhantomData; + +/// A type `T` that can be generated for a given version `V`. +pub trait Generate<T, V: Version> { + /// Generate `T`. + fn generate() -> Result<T, Error>; +} + +#[derive(Clone)] +/// A symmetric key used for `.local` tokens, given a version `V`. +pub struct SymmetricKey<V> { + pub(crate) bytes: Vec<u8>, + pub(crate) phantom: PhantomData<V>, +} + +impl<V: Version> SymmetricKey<V> { + /// Create a `SymmetricKey` from `bytes`. + pub fn from(bytes: &[u8]) -> Result<Self, Error> { + V::validate_local_key(bytes)?; + + Ok(Self { + bytes: bytes.to_vec(), + phantom: PhantomData, + }) + } + + /// Return this as a byte-slice. + pub fn as_bytes(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +impl<V> Drop for SymmetricKey<V> { + fn drop(&mut self) { + use zeroize::Zeroize; + self.bytes.iter_mut().zeroize(); + } +} + +impl<V> Debug for SymmetricKey<V> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "SymmetricKey {{***OMITTED***}}") + } +} + +impl<V: Version> PartialEq<SymmetricKey<V>> for SymmetricKey<V> { + fn eq(&self, other: &SymmetricKey<V>) -> bool { + use subtle::ConstantTimeEq; + self.as_bytes().ct_eq(other.as_bytes()).into() + } +} + +#[derive(Clone)] +/// An asymmetric secret key used for `.public` tokens, given a version `V`. +/// +/// In case of Ed25519, which is used in V2 and V4, this is the seed concatenated with the public key. +pub struct AsymmetricSecretKey<V> { + pub(crate) bytes: Vec<u8>, + pub(crate) phantom: PhantomData<V>, +} + +impl<V: Version> AsymmetricSecretKey<V> { + /// Create a `AsymmetricSecretKey` from `bytes`. + /// + /// __PANIC__: If the version is V2 or V4, a panic will occur if an all-zero + /// secret seed is used. + pub fn from(bytes: &[u8]) -> Result<Self, Error> { + V::validate_secret_key(bytes)?; + + Ok(Self { + bytes: bytes.to_vec(), + phantom: PhantomData, + }) + } + + /// Return this as a byte-slice. + pub fn as_bytes(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +impl<V> Drop for AsymmetricSecretKey<V> { + fn drop(&mut self) { + use zeroize::Zeroize; + self.bytes.iter_mut().zeroize(); + } +} + +impl<V> Debug for AsymmetricSecretKey<V> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "AsymmetricSecretKey {{***OMITTED***}}") + } +} + +impl<V: Version> PartialEq<AsymmetricSecretKey<V>> for AsymmetricSecretKey<V> { + fn eq(&self, other: &AsymmetricSecretKey<V>) -> bool { + use subtle::ConstantTimeEq; + self.as_bytes().ct_eq(other.as_bytes()).into() + } +} + +#[derive(Debug, Clone)] +/// An asymmetric public key used for `.public` tokens, given a version `V`. +pub struct AsymmetricPublicKey<V> { + pub(crate) bytes: Vec<u8>, + pub(crate) phantom: PhantomData<V>, +} + +impl<V: Version> AsymmetricPublicKey<V> { + /// Create a `AsymmetricPublicKey` from `bytes`. + pub fn from(bytes: &[u8]) -> Result<Self, Error> { + V::validate_public_key(bytes)?; + + Ok(Self { + bytes: bytes.to_vec(), + phantom: PhantomData, + }) + } + + /// Return this as a byte-slice. + pub fn as_bytes(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +impl<V: Version> PartialEq<AsymmetricPublicKey<V>> for AsymmetricPublicKey<V> { + fn eq(&self, other: &AsymmetricPublicKey<V>) -> bool { + use subtle::ConstantTimeEq; + self.as_bytes().ct_eq(other.as_bytes()).into() + } +} + +#[derive(Debug, Clone)] +/// A keypair of an [`AsymmetricSecretKey`] and its corresponding [`AsymmetricPublicKey`]. +pub struct AsymmetricKeyPair<V> { + /// The [`AsymmetricSecretKey`]. + pub public: AsymmetricPublicKey<V>, + /// The [`AsymmetricPublicKey`]. + pub secret: AsymmetricSecretKey<V>, +} diff --git a/vendor/pasetors/src/lib.rs b/vendor/pasetors/src/lib.rs new file mode 100644 index 000000000..a921e9272 --- /dev/null +++ b/vendor/pasetors/src/lib.rs @@ -0,0 +1,360 @@ +//! # Getting started +//! This library has two ways of working with tokens. The first is the [`local`] and [`public`] module, +//! which the below examples make use of. These use the latest version of PASETO for tokens, +//! along with [`claims::Claims`], to enable a straightforward way of defining common claims. +//! [`claims::ClaimsValidationRules`] lets you define validation rules, that are covered when using +//! the [`local`] and [`public`] module. Using these modules means that validation of registered +//! claims is handled automatically. +//! +//! If more control over the input is needed, and validation is handled manually, the [`version4`]/[`version2`] +//! module provide a lower-level interface, where payloads are be provided as byte-slices. +//! +//! NOTE: [`claims`], [`local`] and [`public`] modules are __only available with default-features enabled__. +//! ## Creating and verifying public tokens +//! ```rust +//! use pasetors::claims::{Claims, ClaimsValidationRules}; +//! use pasetors::keys::{Generate, AsymmetricKeyPair, AsymmetricSecretKey, AsymmetricPublicKey}; +//! use pasetors::{public, Public, version4::V4}; +//! use pasetors::token::{UntrustedToken, TrustedToken}; +//! use core::convert::TryFrom; +//! +//! // Setup the default claims, which include `iat` and `nbf` as the current time and `exp` of one hour. +//! // Add a custom `data` claim as well. +//! let mut claims = Claims::new()?; +//! claims.add_additional("data", "A public, signed message")?; +//! +//! // Generate the keys and sign the claims. +//! let kp = AsymmetricKeyPair::<V4>::generate()?; +//! let pub_token = public::sign(&kp.secret, &claims, None, Some(b"implicit assertion"))?; +//! +//! // Decide how we want to validate the claims after verifying the token itself. +//! // The default verifies the `nbf`, `iat` and `exp` claims. `nbf` and `iat` are always +//! // expected to be present. +//! // NOTE: Custom claims, defined through `add_additional()`, are not validated. This must be done +//! // manually. +//! let validation_rules = ClaimsValidationRules::new(); +//! let untrusted_token = UntrustedToken::<Public, V4>::try_from(&pub_token)?; +//! let trusted_token = public::verify(&kp.public, &untrusted_token, &validation_rules, None, Some(b"implicit assertion"))?; +//! assert_eq!(&claims, trusted_token.payload_claims().unwrap()); +//! +//! let claims = trusted_token.payload_claims().unwrap(); +//! +//! println!("{:?}", claims.get_claim("data")); +//! println!("{:?}", claims.get_claim("iat")); +//! +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` + +//! ## Creating and verifying local tokens +//! ```rust +//! use pasetors::claims::{Claims, ClaimsValidationRules}; +//! use pasetors::keys::{Generate, SymmetricKey}; +//! use pasetors::{local, Local, version4::V4}; +//! use pasetors::token::UntrustedToken; +//! use core::convert::TryFrom; +//! +//! // Setup the default claims, which include `iat` and `nbf` as the current time and `exp` of one hour. +//! // Add a custom `data` claim as well. +//! let mut claims = Claims::new()?; +//! claims.add_additional("data", "A secret, encrypted message")?; +//! +//! // Generate the key and encrypt the claims. +//! let sk = SymmetricKey::<V4>::generate()?; +//! let token = local::encrypt(&sk, &claims, None, Some(b"implicit assertion"))?; +//! +//! // Decide how we want to validate the claims after verifying the token itself. +//! // The default verifies the `nbf`, `iat` and `exp` claims. `nbf` and `iat` are always +//! // expected to be present. +//! // NOTE: Custom claims, defined through `add_additional()`, are not validated. This must be done +//! // manually. +//! let validation_rules = ClaimsValidationRules::new(); +//! let untrusted_token = UntrustedToken::<Local, V4>::try_from(&token)?; +//! let trusted_token = local::decrypt(&sk, &untrusted_token, &validation_rules, None, Some(b"implicit assertion"))?; +//! assert_eq!(&claims, trusted_token.payload_claims().unwrap()); +//! +//! let claims = trusted_token.payload_claims().unwrap(); +//! +//! println!("{:?}", claims.get_claim("data")); +//! println!("{:?}", claims.get_claim("iat")); +//! +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` + +//! ## Additional claims and their validation +//! +//! ### Setting registered claims and how to validate them +//! ```rust +//! use pasetors::claims::{Claims, ClaimsValidationRules}; +//! +//! // `iat`, `nbf` and `exp` have been set automatically, but could also be overridden. +//! let mut claims = Claims::new()?; +//! claims.issuer("paragonie.com")?; +//! claims.subject("test")?; +//! claims.audience("pie-hosted.com")?; +//! claims.expiration("2039-01-01T00:00:00+00:00")?; +//! claims.not_before("2038-04-01T00:00:00+00:00")?; +//! claims.issued_at("2038-03-17T00:00:00+00:00")?; +//! claims.token_identifier("87IFSGFgPNtQNNuw0AtuLttPYFfYwOkjhqdWcLoYQHvL")?; +//! +//! let mut validation_rules = ClaimsValidationRules::new(); +//! validation_rules.validate_issuer_with("paragonie.com"); +//! validation_rules.validate_subject_with("test"); +//! validation_rules.validate_audience_with("pie-hosted.com"); +//! validation_rules.validate_token_identifier_with("87IFSGFgPNtQNNuw0AtuLttPYFfYwOkjhqdWcLoYQHvL"); +//! +//! // The token has been set to be issued in the future and not valid yet, so validation fails. +//! assert!(validation_rules.validate_claims(&claims).is_err()); +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` +//! ### Non-expiring tokens +//! ```rust +//! use pasetors::claims::{Claims, ClaimsValidationRules}; +//! +//! // Non-expiring tokens +//! let mut claims = Claims::new()?; +//! claims.add_additional("data", "A public, signed message")?; +//! claims.non_expiring(); +//! // Now claims can be validated as non-expiring when we define the validation rule as: +//! let mut validation_rules = ClaimsValidationRules::new(); +//! validation_rules.allow_non_expiring(); +//! +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` + +//! ## Footer with registered and custom claims +//! ```rust +//! use pasetors::paserk::{FormatAsPaserk, Id}; +//! use pasetors::claims::{Claims, ClaimsValidationRules}; +//! use pasetors::footer::Footer; +//! use pasetors::keys::{Generate, AsymmetricKeyPair}; +//! use pasetors::{public, Public, version4::V4}; +//! use pasetors::token::UntrustedToken; +//! use core::convert::TryFrom; +//! +//! // Generate the key used to later sign a token. +//! let kp = AsymmetricKeyPair::<V4>::generate()?; +//! // Serialize the public key to PASERK "pid". +//! let mut pid = Id::from(&kp.public); +//! // Add the "pid" to the "kid" claim of a footer. +//! let mut footer = Footer::new(); +//! footer.key_id(&pid); +//! footer.add_additional("custom_footer_claim", "custom_value")?; +//! +//! let mut claims = Claims::new()?; +//! let pub_token = public::sign(&kp.secret, &claims, Some(&footer), Some(b"implicit assertion"))?; +//! +//! // If we receive a token that needs to be verified, we can still try to parse a Footer from it +//! // as long one was used during creation, if we don't know it beforehand. +//! let validation_rules = ClaimsValidationRules::new(); +//! let untrusted_token = UntrustedToken::<Public, V4>::try_from(&pub_token)?; +//! let trusted_token = public::verify(&kp.public, &untrusted_token, &validation_rules, None, Some(b"implicit assertion"))?; +//! let trusted_footer = Footer::try_from(&trusted_token)?; +//! +//! let mut kid = String::new(); +//! pid.fmt(&mut kid).unwrap(); +//! assert_eq!(trusted_footer.get_claim("kid").unwrap().as_str().unwrap(), kid); +//! +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` + +//! ## PASERK serialization +//! ```rust +//! use pasetors::paserk::FormatAsPaserk; +//! use pasetors::keys::{Generate, SymmetricKey}; +//! use pasetors::version4::V4; +//! use core::convert::TryFrom; +//! +//! // Generate the key and serialize to and from PASERK. +//! let sk = SymmetricKey::<V4>::generate()?; +//! let mut paserk = String::new(); +//! sk.fmt(&mut paserk).unwrap(); +//! let sk = SymmetricKey::<V4>::try_from(paserk.as_str())?; +//! +//! # Ok::<(), pasetors::errors::Error>(()) +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![deny(clippy::mem_forget)] +#![warn( + missing_docs, + rust_2018_idioms, + trivial_casts, + unused_qualifications, + overflowing_literals +)] +#![doc(html_root_url = "https://docs.rs/pasetors/0.6.5")] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[macro_use] +extern crate alloc; + +mod pae; + +/// Errors for token operations. +pub mod errors; + +mod common; + +#[cfg(feature = "std")] +/// Claims for tokens and validation thereof. +pub mod claims; + +#[cfg(feature = "std")] +/// Footer for tokens. +pub mod footer; + +/// Keys used for PASETO tokens. +pub mod keys; + +#[cfg(feature = "paserk")] +/// PASERK key-wrapping and serialization. +pub mod paserk; + +#[cfg(feature = "v2")] +/// PASETO version 2 tokens. +pub mod version2; + +#[cfg(feature = "v3")] +/// PASETO version 3 tokens. +pub mod version3; + +#[cfg(feature = "v4")] +/// PASETO version 4 tokens. +pub mod version4; + +/// Types for handling tokens. +pub mod token; + +#[cfg(feature = "serde")] +/// Serialization and deserialization support for various types. +mod serde; + +mod version; + +/// Public and local tokens. +pub use token::{Local, Public}; + +#[cfg_attr(docsrs, doc(cfg(all(feature = "std", feature = "v4"))))] +#[cfg(all(feature = "std", feature = "v4"))] +/// PASETO public tokens with [`version4`], using [`claims::Claims`]. +pub mod public { + use super::*; + use crate::claims::{Claims, ClaimsValidationRules}; + use crate::errors::Error; + use crate::footer::Footer; + use crate::keys::{AsymmetricPublicKey, AsymmetricSecretKey}; + use crate::token::{TrustedToken, UntrustedToken}; + use crate::version4::V4; + + /// Create a public token using the latest PASETO version (v4). + pub fn sign( + secret_key: &AsymmetricSecretKey<V4>, + message: &Claims, + footer: Option<&Footer>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + match footer { + Some(f) => crate::version4::PublicToken::sign( + secret_key, + message.to_string()?.as_bytes(), + Some(f.to_string()?.as_bytes()), + implicit_assert, + ), + None => crate::version4::PublicToken::sign( + secret_key, + message.to_string()?.as_bytes(), + None, + implicit_assert, + ), + } + } + + /// Verify a public token using the latest PASETO version (v4). If verification passes, + /// validate the claims according to the `validation_rules`. + pub fn verify( + public_key: &AsymmetricPublicKey<V4>, + token: &UntrustedToken<Public, V4>, + validation_rules: &ClaimsValidationRules, + footer: Option<&Footer>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + let mut trusted_token = match footer { + Some(f) => crate::version4::PublicToken::verify( + public_key, + token, + Some(f.to_string()?.as_bytes()), + implicit_assert, + )?, + None => crate::version4::PublicToken::verify(public_key, token, None, implicit_assert)?, + }; + + let claims = Claims::from_string(trusted_token.payload())?; + validation_rules.validate_claims(&claims)?; + trusted_token.set_payload_claims(claims); + + Ok(trusted_token) + } +} + +#[cfg_attr(docsrs, doc(cfg(all(feature = "std", feature = "v4"))))] +#[cfg(all(feature = "std", feature = "v4"))] +/// PASETO local tokens with [`version4`], using [`claims::Claims`]. +pub mod local { + use super::*; + use crate::claims::{Claims, ClaimsValidationRules}; + use crate::errors::Error; + use crate::footer::Footer; + use crate::keys::SymmetricKey; + use crate::token::{TrustedToken, UntrustedToken}; + use crate::version4::V4; + + /// Create a local token using the latest PASETO version (v4). + pub fn encrypt( + secret_key: &SymmetricKey<V4>, + message: &Claims, + footer: Option<&Footer>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + match footer { + Some(f) => crate::version4::LocalToken::encrypt( + secret_key, + message.to_string()?.as_bytes(), + Some(f.to_string()?.as_bytes()), + implicit_assert, + ), + None => crate::version4::LocalToken::encrypt( + secret_key, + message.to_string()?.as_bytes(), + None, + implicit_assert, + ), + } + } + + /// Verify a local token using the latest PASETO version (v4). If verification passes, + /// validate the claims according to the `validation_rules`. + pub fn decrypt( + secret_key: &SymmetricKey<V4>, + token: &UntrustedToken<Local, V4>, + validation_rules: &ClaimsValidationRules, + footer: Option<&Footer>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + let mut trusted_token = match footer { + Some(f) => crate::version4::LocalToken::decrypt( + secret_key, + token, + Some(f.to_string()?.as_bytes()), + implicit_assert, + )?, + None => crate::version4::LocalToken::decrypt(secret_key, token, None, implicit_assert)?, + }; + + let claims = Claims::from_string(trusted_token.payload())?; + validation_rules.validate_claims(&claims)?; + trusted_token.set_payload_claims(claims); + + Ok(trusted_token) + } +} diff --git a/vendor/pasetors/src/pae.rs b/vendor/pasetors/src/pae.rs new file mode 100644 index 000000000..e72e849b0 --- /dev/null +++ b/vendor/pasetors/src/pae.rs @@ -0,0 +1,78 @@ +use crate::errors::Error; +use alloc::vec::Vec; +use core::convert::TryInto; + +/// Encode `n` to little-endian bytes. The MSB is cleared. +pub fn le64(n: u64) -> [u8; core::mem::size_of::<u64>()] { + let mut out = [0u8; core::mem::size_of::<u64>()]; + let mut n_tmp = n; + + out[0] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[1] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[2] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[3] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[4] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[5] = (n_tmp & 255) as u8; + n_tmp >>= 8; + out[6] = (n_tmp & 255) as u8; + n_tmp >>= 8; + n_tmp &= 127; // Clear the MSB for interoperability + out[7] = (n_tmp & 255) as u8; + + out +} + +/// Pre-Authentication Encoding. See [specification](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition). +pub fn pae(pieces: &[&[u8]]) -> Result<Vec<u8>, Error> { + let mut out: Vec<u8> = Vec::with_capacity(64); + + out.extend_from_slice(&le64(pieces.len().try_into()?)); + for elem in pieces.iter() { + out.extend_from_slice(&le64(elem.len().try_into()?)); + out.extend_from_slice(elem); + } + + Ok(out) +} + +#[cfg(test)] +mod unit_tests { + use super::*; + + #[test] + fn test_le64() { + assert_eq!(vec![0, 0, 0, 0, 0, 0, 0, 0], le64(0)); + assert_eq!(vec![10, 0, 0, 0, 0, 0, 0, 0], le64(10)); + } + + #[test] + fn test_pae() { + // Source: https://github.com/paragonie/paseto/blob/master/tests/UtilTest.php + assert_eq!("0000000000000000", hex::encode(&pae(&[]).unwrap())); + assert_eq!( + "01000000000000000000000000000000", + hex::encode(&pae(&[b""]).unwrap()) + ); + assert_eq!( + "020000000000000000000000000000000000000000000000", + hex::encode(&pae(&[b"", b""]).unwrap()) + ); + assert_eq!( + "0100000000000000070000000000000050617261676f6e", + hex::encode(&pae(&[b"Paragon"]).unwrap()) + ); + assert_eq!( + "0200000000000000070000000000000050617261676f6e0a00000000000000496e6974696174697665", + hex::encode(&pae(&[b"Paragon", b"Initiative",]).unwrap()) + ); + assert_eq!( + "0100000000000000190000000000000050617261676f6e0a00000000000000496e6974696174697665", + hex::encode(&pae(&[b"Paragon\n\0\0\0\0\0\0\0Initiative"]).unwrap()) + ); + } +} diff --git a/vendor/pasetors/src/paserk.rs b/vendor/pasetors/src/paserk.rs new file mode 100644 index 000000000..95c54b797 --- /dev/null +++ b/vendor/pasetors/src/paserk.rs @@ -0,0 +1,860 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "paserk")))] + +use crate::common::{decode_b64, encode_b64}; +use crate::errors::Error; +use crate::keys::{AsymmetricPublicKey, AsymmetricSecretKey, SymmetricKey}; +use crate::version::private::Version; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::fmt::Write; +use core::marker::PhantomData; +use orion::hazardous::hash::blake2::blake2b; +use zeroize::Zeroize; + +#[cfg(feature = "v2")] +use crate::version2::V2; + +#[cfg(feature = "v3")] +use crate::version3::V3; +#[cfg(feature = "v3")] +use orion::hazardous::hash::sha2::sha384; + +#[cfg(feature = "v4")] +use crate::version4::V4; + +/// Validate an input string to check if it is a well-formatted PASERK. +/// +/// Return the base64-encoded part of the serialized string. +fn validate_paserk_string( + input: &str, + version_id: &str, + type_id: &str, + expected_len: usize, +) -> Result<Vec<u8>, Error> { + let split = input.split('.').collect::<Vec<&str>>(); + if split.len() != 3 { + return Err(Error::PaserkParsing); + } + + if split[0] == version_id && split[1] == type_id { + let ret = decode_b64(split[2])?; + if ret.len() != expected_len { + return Err(Error::PaserkParsing); + } + + Ok(ret) + } else { + Err(Error::PaserkParsing) + } +} + +/// A trait for serializing a type as PASERK. +pub trait FormatAsPaserk { + /// Format a key as PASERK. + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result; +} + +#[cfg(feature = "v2")] +impl FormatAsPaserk for SymmetricKey<V2> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k2.local.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v2")] +impl TryFrom<&str> for SymmetricKey<V2> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + Ok(Self { + bytes: validate_paserk_string(value, "k2", "local", V2::LOCAL_KEY)?, + phantom: PhantomData, + }) + } +} + +#[cfg(feature = "v4")] +impl FormatAsPaserk for SymmetricKey<V4> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k4.local.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v4")] +impl TryFrom<&str> for SymmetricKey<V4> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + Ok(Self { + bytes: validate_paserk_string(value, "k4", "local", V4::LOCAL_KEY)?, + phantom: PhantomData, + }) + } +} + +#[cfg(feature = "v2")] +impl FormatAsPaserk for AsymmetricSecretKey<V2> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k2.secret.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v2")] +impl TryFrom<&str> for AsymmetricSecretKey<V2> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let mut buf = validate_paserk_string(value, "k2", "secret", V2::SECRET_KEY)?; + let ret = Self::from(&buf)?; + buf.iter_mut().zeroize(); + + Ok(ret) + } +} + +#[cfg(feature = "v3")] +impl FormatAsPaserk for AsymmetricSecretKey<V3> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k3.secret.")?; + write.write_str(&encode_b64(&self.bytes).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v3")] +impl TryFrom<&str> for AsymmetricSecretKey<V3> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let buf = validate_paserk_string(value, "k3", "secret", V3::SECRET_KEY)?; + let ret = Self { + bytes: buf, + phantom: PhantomData, + }; + + Ok(ret) + } +} + +#[cfg(feature = "v4")] +impl FormatAsPaserk for AsymmetricSecretKey<V4> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k4.secret.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v4")] +impl TryFrom<&str> for AsymmetricSecretKey<V4> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let mut buf = validate_paserk_string(value, "k4", "secret", V4::SECRET_KEY)?; + let ret = Self::from(&buf)?; + buf.iter_mut().zeroize(); + + Ok(ret) + } +} + +#[cfg(feature = "v2")] +impl FormatAsPaserk for AsymmetricPublicKey<V2> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k2.public.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v2")] +impl TryFrom<&str> for AsymmetricPublicKey<V2> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + Ok(Self { + bytes: validate_paserk_string(value, "k2", "public", V2::PUBLIC_KEY)?, + phantom: PhantomData, + }) + } +} + +#[cfg(feature = "v3")] +impl FormatAsPaserk for AsymmetricPublicKey<V3> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k3.public.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v3")] +impl TryFrom<&str> for AsymmetricPublicKey<V3> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + Ok(Self { + bytes: validate_paserk_string(value, "k3", "public", V3::PUBLIC_KEY)?, + phantom: PhantomData, + }) + } +} + +#[cfg(feature = "v4")] +impl FormatAsPaserk for AsymmetricPublicKey<V4> { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str("k4.public.")?; + write.write_str(&encode_b64(self.as_bytes()).map_err(|_| core::fmt::Error)?) + } +} + +#[cfg(feature = "v4")] +impl TryFrom<&str> for AsymmetricPublicKey<V4> { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + Ok(Self { + bytes: validate_paserk_string(value, "k4", "public", V4::PUBLIC_KEY)?, + phantom: PhantomData, + }) + } +} + +#[derive(Debug, Clone)] +/// PASERK IDs. +/// +/// This operation calculates the unique ID for a given PASERK. +/// +/// See: <https://github.com/paseto-standard/paserk/blob/master/operations/ID.md> +pub struct Id { + header: String, + identifier: String, +} + +impl PartialEq<Id> for Id { + fn eq(&self, other: &Id) -> bool { + use subtle::ConstantTimeEq; + (self.header.as_bytes().ct_eq(other.header.as_bytes()) + & self + .identifier + .as_bytes() + .ct_eq(other.identifier.as_bytes())) + .into() + } +} + +#[cfg(feature = "v3")] +impl From<&AsymmetricSecretKey<V3>> for Id { + fn from(key: &AsymmetricSecretKey<V3>) -> Self { + let header = String::from("k3.sid."); + let mut hasher = sha384::Sha384::new(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(&hasher.finalize().unwrap().as_ref()[..33]).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v3")] +impl From<&AsymmetricPublicKey<V3>> for Id { + fn from(key: &AsymmetricPublicKey<V3>) -> Self { + let header = String::from("k3.pid."); + let mut hasher = sha384::Sha384::new(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(&hasher.finalize().unwrap().as_ref()[..33]).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v2")] +impl From<&SymmetricKey<V2>> for Id { + fn from(key: &SymmetricKey<V2>) -> Self { + let header = String::from("k2.lid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v4")] +impl From<&SymmetricKey<V4>> for Id { + fn from(key: &SymmetricKey<V4>) -> Self { + let header = String::from("k4.lid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v2")] +impl From<&AsymmetricSecretKey<V2>> for Id { + fn from(key: &AsymmetricSecretKey<V2>) -> Self { + let header = String::from("k2.sid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v4")] +impl From<&AsymmetricSecretKey<V4>> for Id { + fn from(key: &AsymmetricSecretKey<V4>) -> Self { + let header = String::from("k4.sid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v2")] +impl From<&AsymmetricPublicKey<V2>> for Id { + fn from(key: &AsymmetricPublicKey<V2>) -> Self { + let header = String::from("k2.pid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +#[cfg(feature = "v4")] +impl From<&AsymmetricPublicKey<V4>> for Id { + fn from(key: &AsymmetricPublicKey<V4>) -> Self { + let header = String::from("k4.pid."); + let mut hasher = blake2b::Blake2b::new(33).unwrap(); + hasher.update(header.as_bytes()).unwrap(); + + let mut paserk_string = String::new(); + key.fmt(&mut paserk_string).unwrap(); + hasher.update(paserk_string.as_bytes()).unwrap(); + let identifier = encode_b64(hasher.finalize().unwrap().as_ref()).unwrap(); + debug_assert_eq!(identifier.len(), 44); + + Self { header, identifier } + } +} + +impl FormatAsPaserk for Id { + fn fmt(&self, write: &mut dyn Write) -> core::fmt::Result { + write.write_str(&self.header)?; + write.write_str(&self.identifier) + } +} + +#[cfg(any(feature = "v2", feature = "v3", feature = "v4"))] +impl TryFrom<&str> for Id { + type Error = Error; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let split = value.split('.').collect::<Vec<&str>>(); + if split.len() != 3 { + return Err(Error::PaserkParsing); + } + + let header = match (split[0], split[1]) { + ("k2", "lid" | "sid" | "pid") + | ("k3", "sid" | "pid") + | ("k4", "lid" | "sid" | "pid") => format!("{}.{}.", split[0], split[1]), + _ => return Err(Error::PaserkParsing), + }; + + let expected_len = match split[0] { + #[cfg(feature = "v2")] + "k2" => V2::PASERK_ID, + #[cfg(feature = "v3")] + "k3" => V3::PASERK_ID, + #[cfg(feature = "v4")] + "k4" => V4::PASERK_ID, + _ => return Err(Error::PaserkParsing), + }; + if split[2].len() != expected_len { + return Err(Error::PaserkParsing); + } + + Ok(Self { + header, + identifier: split[2].to_string(), + }) + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod tests { + use super::*; + + use ::serde::{Deserialize, Serialize}; + use alloc::string::String; + use alloc::vec::Vec; + use hex; + use std::fs::File; + use std::io::BufReader; + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct TestFile { + pub(crate) name: String, + pub(crate) tests: Vec<PaserkTest>, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct PaserkTest { + pub(crate) name: String, + #[serde(rename(deserialize = "expect-fail"))] + pub(crate) expect_fail: bool, + pub(crate) key: Option<String>, + pub(crate) paserk: Option<String>, + #[serde(rename(deserialize = "public-key"))] + pub(crate) public_key: Option<String>, + #[serde(rename(deserialize = "secret-key-seed"))] + pub(crate) secret_key_seed: Option<String>, + } + + const TEST_WITH_ALL_ZERO_SEED: [&str; 4] = + ["k2.secret-1", "k2.sid-1", "k4.secret-1", "k4.sid-1"]; + + macro_rules! test_paserk_type { + ($test_func_name:ident, $key:ident, $version:ident, $path:expr) => { + #[test] + pub fn $test_func_name() { + let file = File::open($path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for test_paserk in tests.tests { + if TEST_WITH_ALL_ZERO_SEED.contains(&test_paserk.name.as_str()) { + // We require that the public key match the secret seed. Thus, + // the first test vectors for PASERK dealing with secret keys + // will always fail. + continue; + } + + match (test_paserk.expect_fail, test_paserk.paserk, test_paserk.key) { + (true, Some(_paserk), Some(_key)) => { + unreachable!("This test vectors shouldn't exist") + } + (true, Some(paserk), None) => { + assert!($key::<$version>::try_from(paserk.as_str()).is_err()); + continue; + } + (true, None, Some(key)) => { + if hex::decode(&key).is_err() { + continue; // The case where RSA keys are put in v2 + } + assert!($key::<$version>::from(&hex::decode(&key).unwrap()).is_err()); + continue; + } + (false, Some(paserk), Some(key)) => { + #[cfg(feature = "serde")] + let key_hex = key.clone(); + let deser = $key::<$version>::try_from(paserk.as_str()).unwrap(); + let key = $key::<$version>::from(&hex::decode(&key).unwrap()).unwrap(); + assert_eq!(deser.as_bytes(), key.as_bytes()); + let mut buf = String::new(); + key.fmt(&mut buf).unwrap(); + assert_eq!(paserk, buf); + + #[cfg(feature = "serde")] + { + let deser: $key<$version> = + serde_json::from_str(&format!(r#""{paserk}""#)).unwrap(); + let key = $key::<$version>::from(&hex::decode(&key_hex).unwrap()) + .unwrap(); + assert_eq!(deser.as_bytes(), key.as_bytes()); + let ser = serde_json::to_string(&key).unwrap(); + assert_eq!(format!(r#""{paserk}""#), ser); + } + } + _ => unreachable!("This test vectors shouldn't exist"), + } + } + } + }; + } + + macro_rules! test_id_type { + ($test_func_name:ident, $key:ident, $version:ident, $path:expr) => { + #[test] + pub fn $test_func_name() { + let file = File::open($path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for test_paserk in tests.tests { + if TEST_WITH_ALL_ZERO_SEED.contains(&test_paserk.name.as_str()) { + // We require that the public key match the secret seed. Thus, + // the first test vectors for PASERK dealing with secret keys + // will always fail. + continue; + } + + match (test_paserk.expect_fail, test_paserk.paserk, test_paserk.key) { + (true, Some(_paserk), Some(_key)) => { + unreachable!("This test vectors shouldn't exist") + } + (true, Some(_paserk), None) => { + unreachable!("This test vectors shouldn't exist") + } + (true, None, Some(key)) => { + if hex::decode(&key).is_err() { + continue; // The case where RSA keys are put in v2 + } + assert!($key::<$version>::from(&hex::decode(&key).unwrap()).is_err()); + continue; + } + (false, Some(paserk), Some(key)) => { + #[cfg(feature = "serde")] + let key_hex = key.clone(); + let key = $key::<$version>::from(&hex::decode(&key).unwrap()).unwrap(); + + let paserk_id = Id::from(&key); + let mut buf = String::new(); + paserk_id.fmt(&mut buf).unwrap(); + assert_eq!(paserk, buf); + + #[cfg(feature = "serde")] + { + let key = $key::<$version>::from(&hex::decode(&key_hex).unwrap()) + .unwrap(); + let paserk_id = Id::from(&key); + let mut buf = String::new(); + paserk_id.fmt(&mut buf).unwrap(); + + let deser: Id = + serde_json::from_str(&format!(r#""{buf}""#)).unwrap(); + assert_eq!(paserk_id, deser); + let ser = serde_json::to_string(&paserk_id).unwrap(); + assert_eq!(format!(r#""{buf}""#), ser); + } + } + _ => unreachable!("This test vectors shouldn't exist"), + } + } + } + }; + } + + #[cfg(test)] + #[cfg(feature = "v2")] + mod v2 { + use super::*; + + test_id_type!( + test_local_k2_id, + SymmetricKey, + V2, + "./test_vectors/PASERK/k2.lid.json" + ); + + test_id_type!( + test_secret_k2_id, + AsymmetricSecretKey, + V2, + "./test_vectors/PASERK/k2.sid.json" + ); + + test_id_type!( + test_public_k2_id, + AsymmetricPublicKey, + V2, + "./test_vectors/PASERK/k2.pid.json" + ); + + test_paserk_type!( + test_local_k2, + SymmetricKey, + V2, + "./test_vectors/PASERK/k2.local.json" + ); + + test_paserk_type!( + test_public_k2, + AsymmetricPublicKey, + V2, + "./test_vectors/PASERK/k2.public.json" + ); + + test_paserk_type!( + test_secret_k2, + AsymmetricSecretKey, + V2, + "./test_vectors/PASERK/k2.secret.json" + ); + + #[test] + fn test_wrong_version_or_purpose() { + assert!(SymmetricKey::<V2>::try_from( + "k2.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_ok()); + assert!(SymmetricKey::<V2>::try_from( + "k4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(SymmetricKey::<V2>::try_from( + "k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(SymmetricKey::<V2>::try_from( + "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + + assert!(AsymmetricPublicKey::<V2>::try_from( + "k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_ok()); + assert!(AsymmetricPublicKey::<V2>::try_from( + "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V2>::try_from( + "k2.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V2>::try_from( + "k4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + + assert!(AsymmetricSecretKey::<V2>::try_from("k2.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_ok()); + assert!(AsymmetricSecretKey::<V2>::try_from("k4.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + assert!(AsymmetricSecretKey::<V2>::try_from("k2.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + assert!(AsymmetricSecretKey::<V2>::try_from("k4.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + } + } + + #[cfg(test)] + #[cfg(feature = "v3")] + mod v3 { + use super::*; + + test_id_type!( + test_secret_k3_id, + AsymmetricSecretKey, + V3, + "./test_vectors/PASERK/k3.sid.json" + ); + + test_id_type!( + test_public_k3_id, + AsymmetricPublicKey, + V3, + "./test_vectors/PASERK/k3.pid.json" + ); + + test_paserk_type!( + test_public_k3, + AsymmetricPublicKey, + V3, + "./test_vectors/PASERK/k3.public.json" + ); + + test_paserk_type!( + test_secret_k3, + AsymmetricSecretKey, + V3, + "./test_vectors/PASERK/k3.secret.json" + ); + + #[test] + fn test_wrong_version_or_purpose() { + assert!(AsymmetricPublicKey::<V3>::try_from( + "k3.public.AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_ok()); + assert!(AsymmetricPublicKey::<V3>::try_from( + "k4.public.AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V3>::try_from( + "k3.local.AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V3>::try_from( + "k4.local.AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + + assert!(AsymmetricSecretKey::<V3>::try_from( + "k3.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" + ) + .is_ok()); + assert!(AsymmetricSecretKey::<V3>::try_from( + "k4.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" + ) + .is_err()); + assert!(AsymmetricSecretKey::<V3>::try_from( + "k3.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" + ) + .is_err()); + assert!(AsymmetricSecretKey::<V3>::try_from( + "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" + ) + .is_err()); + } + } + + #[cfg(test)] + #[cfg(feature = "v4")] + mod v4 { + use super::*; + + test_id_type!( + test_local_k4_id, + SymmetricKey, + V4, + "./test_vectors/PASERK/k4.lid.json" + ); + + test_id_type!( + test_secret_k4_id, + AsymmetricSecretKey, + V4, + "./test_vectors/PASERK/k4.sid.json" + ); + + test_id_type!( + test_public_k4_id, + AsymmetricPublicKey, + V4, + "./test_vectors/PASERK/k4.pid.json" + ); + + test_paserk_type!( + test_local_k4, + SymmetricKey, + V4, + "./test_vectors/PASERK/k4.local.json" + ); + + test_paserk_type!( + test_public_k4, + AsymmetricPublicKey, + V4, + "./test_vectors/PASERK/k4.public.json" + ); + + test_paserk_type!( + test_secret_k4, + AsymmetricSecretKey, + V4, + "./test_vectors/PASERK/k4.secret.json" + ); + + #[test] + fn test_wrong_version_or_purpose() { + assert!(SymmetricKey::<V4>::try_from( + "k4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_ok()); + assert!(SymmetricKey::<V4>::try_from( + "k2.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(SymmetricKey::<V4>::try_from( + "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(SymmetricKey::<V4>::try_from( + "k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + + assert!(AsymmetricPublicKey::<V4>::try_from( + "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_ok()); + assert!(AsymmetricPublicKey::<V4>::try_from( + "k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V4>::try_from( + "k4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + assert!(AsymmetricPublicKey::<V4>::try_from( + "k2.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ) + .is_err()); + + assert!(AsymmetricSecretKey::<V4>::try_from("k4.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_ok()); + assert!(AsymmetricSecretKey::<V4>::try_from("k2.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + assert!(AsymmetricSecretKey::<V4>::try_from("k4.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + assert!(AsymmetricSecretKey::<V4>::try_from("k2.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ").is_err()); + } + } + + #[test] + #[cfg(all(feature = "v4", feature = "v3"))] + fn test_partial_eq_id() { + use crate::keys::{AsymmetricKeyPair, Generate}; + + let kpv4 = AsymmetricKeyPair::<V4>::generate().unwrap(); + assert_eq!(Id::from(&kpv4.secret), Id::from(&kpv4.secret)); + assert_ne!(Id::from(&kpv4.secret), Id::from(&kpv4.public)); + let kpv3 = AsymmetricKeyPair::<V3>::generate().unwrap(); + assert_ne!(Id::from(&kpv4.secret), Id::from(&kpv3.secret)); + } + + #[test] + #[cfg(feature = "v4")] + fn test_validate_paserk_string() { + assert!(validate_paserk_string("k4.public", "k4", "public", V4::PUBLIC_KEY).is_err()); + assert!( + validate_paserk_string("k4.public.public.public", "k4", "public", V4::PUBLIC_KEY) + .is_err() + ); + let too_long = format!( + "k4.public.{}", + encode_b64([0u8; V4::PUBLIC_KEY * 2]).unwrap() + ); + assert!(validate_paserk_string(&too_long, "k4", "public", V4::PUBLIC_KEY).is_err()); + } +} diff --git a/vendor/pasetors/src/serde.rs b/vendor/pasetors/src/serde.rs new file mode 100644 index 000000000..92eb5fe89 --- /dev/null +++ b/vendor/pasetors/src/serde.rs @@ -0,0 +1,133 @@ +use crate::keys::{AsymmetricPublicKey, AsymmetricSecretKey, SymmetricKey}; +#[cfg(feature = "paserk")] +use crate::paserk::{FormatAsPaserk, Id}; +use alloc::string::String; +use core::convert::TryFrom; + +#[cfg(all(feature = "paserk", feature = "serde"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "paserk", feature = "serde"))))] +impl<V> serde::Serialize for AsymmetricPublicKey<V> +where + AsymmetricPublicKey<V>: FormatAsPaserk, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::Error; + let mut paserk_string = String::new(); + self.fmt(&mut paserk_string).map_err(S::Error::custom)?; + serializer.serialize_str(&paserk_string) + } +} + +#[cfg(all(feature = "serde", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "std"))))] +impl<'de, V> serde::Deserialize<'de> for AsymmetricPublicKey<V> +where + AsymmetricPublicKey<V>: TryFrom<&'de str>, + <AsymmetricPublicKey<V> as TryFrom<&'de str>>::Error: std::fmt::Display, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let paserk_string = <&str>::deserialize(deserializer)?; + TryFrom::try_from(paserk_string).map_err(serde::de::Error::custom) + } +} + +#[cfg(all(feature = "paserk", feature = "serde"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "paserk", feature = "serde"))))] +impl<V> serde::Serialize for AsymmetricSecretKey<V> +where + AsymmetricSecretKey<V>: FormatAsPaserk, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::Error; + let mut paserk_string = String::new(); + self.fmt(&mut paserk_string).map_err(S::Error::custom)?; + serializer.serialize_str(&paserk_string) + } +} + +#[cfg(all(feature = "serde", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "std"))))] +impl<'de, V> serde::Deserialize<'de> for AsymmetricSecretKey<V> +where + AsymmetricSecretKey<V>: TryFrom<&'de str>, + <AsymmetricSecretKey<V> as TryFrom<&'de str>>::Error: std::fmt::Display, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let paserk_string = <&str>::deserialize(deserializer)?; + TryFrom::try_from(paserk_string).map_err(serde::de::Error::custom) + } +} + +#[cfg(all(feature = "paserk", feature = "serde"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "paserk", feature = "serde"))))] +impl<V> serde::Serialize for SymmetricKey<V> +where + SymmetricKey<V>: FormatAsPaserk, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::Error; + let mut paserk_string = String::new(); + self.fmt(&mut paserk_string).map_err(S::Error::custom)?; + serializer.serialize_str(&paserk_string) + } +} + +#[cfg(all(feature = "serde", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "serde", feature = "std"))))] +impl<'de, V> serde::Deserialize<'de> for SymmetricKey<V> +where + SymmetricKey<V>: TryFrom<&'de str>, + <SymmetricKey<V> as TryFrom<&'de str>>::Error: std::fmt::Display, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let paserk_string = <&str>::deserialize(deserializer)?; + TryFrom::try_from(paserk_string).map_err(serde::de::Error::custom) + } +} + +#[cfg(all(feature = "paserk", feature = "serde"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "paserk", feature = "serde"))))] +impl serde::Serialize for Id { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::Error; + let mut paserk_id = String::new(); + self.fmt(&mut paserk_id).map_err(S::Error::custom)?; + serializer.serialize_str(&paserk_id) + } +} + +#[cfg(all(feature = "paserk", feature = "serde", feature = "std"))] +#[cfg_attr( + docsrs, + doc(cfg(all(feature = "paserk", feature = "serde", feature = "std"))) +)] +impl<'de> serde::Deserialize<'de> for Id { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let paserk_id = <&str>::deserialize(deserializer)?; + TryFrom::try_from(paserk_id).map_err(serde::de::Error::custom) + } +} diff --git a/vendor/pasetors/src/token.rs b/vendor/pasetors/src/token.rs new file mode 100644 index 000000000..ae3368036 --- /dev/null +++ b/vendor/pasetors/src/token.rs @@ -0,0 +1,656 @@ +use crate::alloc::string::ToString; +#[cfg(feature = "std")] +use crate::claims::Claims; +use crate::common; +use crate::errors::Error; +#[cfg(feature = "std")] +use crate::footer::Footer; +use crate::token::private::Purpose; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::marker::PhantomData; + +pub(crate) mod private { + use super::Error; + use crate::version::private::Version; + + // Inside private module to prevent users from implementing this themself. + + /// Purpose (`local`/`public`) of a token, given a version `V`. + pub trait Purpose<V: Version> { + /// Validate the header for a given version and purpose for some token. + fn validate_header(token: &str) -> Result<(), Error>; + /// Validate the tokens raw (decoded base64) + /// message length for a given version and purpose for some token. + fn validate_token_message_len(message: &[u8]) -> Result<(), Error>; + /// Parse the raw payload of a token. Either the ciphertext or the message that was signed. + /// The length **MUST** have been verified beforehand. + fn parse_raw_payload(message: &[u8]) -> &[u8]; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A public token. +pub struct Public; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// A local token. +pub struct Local; + +impl<V: Version> Purpose<V> for Public { + fn validate_header(token: &str) -> Result<(), Error> { + if token.is_empty() || !token.starts_with(V::PUBLIC_HEADER) { + return Err(Error::TokenFormat); + } + + Ok(()) + } + + fn validate_token_message_len(message: &[u8]) -> Result<(), Error> { + if message.len() <= V::PUBLIC_SIG { + // Empty payload encrypted. Disallowed by PASETO + return Err(Error::TokenFormat); + } + + Ok(()) + } + + fn parse_raw_payload(message: &[u8]) -> &[u8] { + debug_assert!(message.len() > V::PUBLIC_SIG); + &message[..message.len() - V::PUBLIC_SIG] + } +} + +impl<V: Version> Purpose<V> for Local { + fn validate_header(token: &str) -> Result<(), Error> { + if token.is_empty() || !token.starts_with(V::LOCAL_HEADER) { + return Err(Error::TokenFormat); + } + + Ok(()) + } + + fn validate_token_message_len(message: &[u8]) -> Result<(), Error> { + if message.len() <= V::LOCAL_NONCE + V::LOCAL_TAG { + // Empty payload encrypted. Disallowed by PASETO + return Err(Error::TokenFormat); + } + + Ok(()) + } + + fn parse_raw_payload(message: &[u8]) -> &[u8] { + debug_assert!(message.len() > V::LOCAL_TAG + V::LOCAL_NONCE); + &message[V::LOCAL_NONCE..message.len() - V::LOCAL_TAG] + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +/// A [`TrustedToken`] is returned by either a `verify()` or `decrypt()` operation and represents +/// a validated token. +/// +/// It represents a authenticated and non-tampered token. It **does not** validate additional things, +/// such as claims that may be within the token payload itself. These must still be validated separately. +/// +/// However, using the [`crate::public`] and [`crate::local`] API will automatically handle claims +/// validation. Any validated claims may be retrieved with [`TrustedToken::payload_claims()`]. +pub struct TrustedToken { + header: String, + // PASETO requires the payload to be valid JSON in UTF-8, so we say String for UTF-8. + payload: String, + #[cfg(feature = "std")] + // If std is available, we also keep claims as JSON. + payload_claims: Option<Claims>, + footer: Vec<u8>, + implicit_assert: Vec<u8>, +} + +impl TrustedToken { + pub(crate) fn _new( + header: &str, + payload: &[u8], + footer: &[u8], + implicit_assert: &[u8], + ) -> Result<Self, Error> { + Ok(Self { + header: header.to_string(), + payload: String::from_utf8(payload.to_vec()).map_err(|_| Error::PayloadInvalidUtf8)?, + #[cfg(feature = "std")] + payload_claims: None, + footer: footer.to_vec(), + implicit_assert: implicit_assert.to_vec(), + }) + } + + /// Get the header that is used for this token. + pub fn header(&self) -> &str { + &self.header + } + + /// Get the payload that is used for this token. + pub fn payload(&self) -> &str { + &self.payload + } + + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + /// Return the optional and validated [`Claims`] parsed from the tokens payload. + /// + /// - `None`: If no [`Claims`] have been parsed or validated. + /// - `Some`: If some [`Claims`] have been parsed **AND** validated. + /// + /// [`Claims`]: crate::claims::Claims + pub fn payload_claims(&self) -> Option<&Claims> { + debug_assert!(self.payload_claims.is_some()); + match &self.payload_claims { + Some(claims) => Some(claims), + None => None, + } + } + + #[cfg(feature = "std")] + /// Set the payload claims **AFTER HAVING VALIDATED THEM**. + pub(crate) fn set_payload_claims(&mut self, claims: Claims) { + self.payload_claims = Some(claims); + } + + /// Get the footer used to create the token. + /// + /// Empty if `None` was used during creation. + pub fn footer(&self) -> &[u8] { + &self.footer + } + + /// Get the implicit assertion used to create the token. + /// + /// Empty if `None` was used during creation. + /// If token was created using `V2`, then it will always be empty. + pub fn implicit_assert(&self) -> &[u8] { + &self.implicit_assert + } +} + +#[cfg(feature = "std")] +impl TryFrom<&TrustedToken> for Footer { + type Error = Error; + + fn try_from(value: &TrustedToken) -> Result<Self, Self::Error> { + if value.footer.is_empty() { + return Err(Error::FooterParsing); + } + + let mut footer = Footer::new(); + footer.parse_bytes(value.footer())?; + + Ok(footer) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// [`UntrustedToken`] can parse PASETO tokens in order to extract individual parts of it. +/// +/// A use-case for this would be parsing the tokens footer, if this is not known before receiving it. Then, +/// the footer can be used during verification/decryption of the token itself. +/// +/// This type should only be used in order to verify the validity of a token. +/// +/// __WARNING__: Anything returned by this type should be treated as **UNTRUSTED** until the token +/// has been verified. +pub struct UntrustedToken<T, V> { + message: Vec<u8>, + footer: Vec<u8>, + phantom_t: PhantomData<T>, + phantom_v: PhantomData<V>, +} + +impl<T: Purpose<V>, V: Version> TryFrom<&str> for UntrustedToken<T, V> { + type Error = Error; + + /// This fails if `value` is not a PASETO token or it has invalid base64 encoding. + fn try_from(value: &str) -> Result<Self, Self::Error> { + T::validate_header(value)?; + + let parts_split = value.split('.').collect::<Vec<&str>>(); + if parts_split.len() < 3 || parts_split.len() > 4 { + return Err(Error::TokenFormat); + } + if parts_split[2].is_empty() { + // Empty payload entirely + return Err(Error::TokenFormat); + } + + let m_raw = common::decode_b64(parts_split[2])?; + T::validate_token_message_len(&m_raw)?; + let is_footer_present = parts_split.len() == 4; + + Ok(Self { + message: m_raw, + footer: { + if is_footer_present { + common::decode_b64(parts_split[3])? + } else { + Vec::<u8>::new() + } + }, + phantom_t: PhantomData, + phantom_v: PhantomData, + }) + } +} + +impl<T: Purpose<V>, V: Version> TryFrom<&String> for UntrustedToken<T, V> { + type Error = Error; + + /// This fails if `value` is not a PASETO token or it has invalid base64 encoding. + fn try_from(value: &String) -> Result<Self, Self::Error> { + Self::try_from(value.as_str()) + } +} + +impl<T: Purpose<V>, V: Version> UntrustedToken<T, V> { + /// Return untrusted message of this [`UntrustedToken`]. + /// If it is a [`Local`] token, this is the encrypted message with nonce and tag. + /// If it is a [`Public`] token, the signature is included. + pub fn untrusted_message(&self) -> &[u8] { + &self.message + } + + /// Return untrusted payload only of this [`UntrustedToken`]'s message body. + /// If it is a [`Local`] token, this is the encrypted message sans nonce and tag. + /// If it is a [`Public`] token, the signature is not included. + pub fn untrusted_payload(&self) -> &[u8] { + T::parse_raw_payload(self.untrusted_message()) + } + + /// Return untrusted footer of this [`UntrustedToken`]. + /// Empty if there was no footer in the token. + pub fn untrusted_footer(&self) -> &[u8] { + &self.footer + } +} + +#[cfg(test)] +#[cfg(all(feature = "v2", feature = "v3", feature = "v4"))] +mod tests_untrusted { + use super::*; + use crate::common::encode_b64; + use crate::errors::Error; + use crate::version::private::Version; + use crate::{version2::V2, version3::V3, version4::V4}; + + const V2_PUBLIC_TOKEN: &str = "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const V2_LOCAL_TOKEN: &str = "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const V3_PUBLIC_TOKEN: &str = "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9"; + const V4_PUBLIC_TOKEN: &str = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const V4_LOCAL_TOKEN: &str = "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + const TOKEN_LIST: [&str; 5] = [ + V2_PUBLIC_TOKEN, + V2_LOCAL_TOKEN, + V3_PUBLIC_TOKEN, + V4_LOCAL_TOKEN, + V4_PUBLIC_TOKEN, + ]; + + fn test_untrusted_parse_fails(invalid: &str, expected_err: Error) { + if invalid.starts_with(V2::LOCAL_HEADER) { + assert_eq!( + UntrustedToken::<Local, V2>::try_from(invalid).unwrap_err(), + expected_err + ); + } + if invalid.starts_with(V2::PUBLIC_HEADER) { + assert_eq!( + UntrustedToken::<Public, V2>::try_from(invalid).unwrap_err(), + expected_err + ); + } + if invalid.starts_with(V3::LOCAL_HEADER) { + assert_eq!( + UntrustedToken::<Local, V3>::try_from(invalid).unwrap_err(), + expected_err + ); + } + if invalid.starts_with(V3::PUBLIC_HEADER) { + assert_eq!( + UntrustedToken::<Public, V3>::try_from(invalid).unwrap_err(), + expected_err + ); + } + if invalid.starts_with(V4::LOCAL_HEADER) { + assert_eq!( + UntrustedToken::<Local, V4>::try_from(invalid).unwrap_err(), + expected_err + ); + } + if invalid.starts_with(V4::PUBLIC_HEADER) { + assert_eq!( + UntrustedToken::<Public, V4>::try_from(invalid).unwrap_err(), + expected_err + ); + } + } + + #[test] + fn empty_string() { + assert_eq!( + UntrustedToken::<Local, V2>::try_from("").unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V2>::try_from("").unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V3>::try_from("").unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V3>::try_from("").unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V4>::try_from("").unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V4>::try_from("").unwrap_err(), + Error::TokenFormat + ); + } + + #[test] + fn no_separators() { + for token in TOKEN_LIST { + let split = token.split('.').collect::<Vec<&str>>(); + let invalid: String = split.iter().copied().collect(); + + test_untrusted_parse_fails(&invalid, Error::TokenFormat); + } + } + + #[test] + // NOTE: See https://github.com/paseto-standard/paseto-spec/issues/17 + fn missing_payload() { + for token in TOKEN_LIST { + let split = token.split('.').collect::<Vec<&str>>(); + let invalid: String = format!("{}.{}..{}", split[0], split[1], split[3]); + + test_untrusted_parse_fails(&invalid, Error::TokenFormat); + } + } + + #[test] + fn payload_too_short() { + for token in TOKEN_LIST { + let split = token.split('.').collect::<Vec<&str>>(); + let invalid: String = format!( + "{}.{}.{}.{}", + split[0], + split[1], + encode_b64(split[0].as_bytes()).unwrap(), + split[3] + ); + + test_untrusted_parse_fails(&invalid, Error::TokenFormat); + } + } + + #[test] + fn extra_after_footer() { + for token in TOKEN_LIST { + let mut invalid = token.to_string(); + invalid.push_str(".shouldNotBeHere"); + + test_untrusted_parse_fails(&invalid, Error::TokenFormat); + } + } + + #[test] + fn invalid_header() { + // Invalid version + assert_eq!( + UntrustedToken::<Public, V2>::try_from(&V2_PUBLIC_TOKEN.replace("v2", "v4")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V2>::try_from(&V2_LOCAL_TOKEN.replace("v2", "v4")).unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V3>::try_from(&V3_PUBLIC_TOKEN.replace("v3", "v2")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V4>::try_from(&V4_LOCAL_TOKEN.replace("v4", "v2")).unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V4>::try_from(&V4_PUBLIC_TOKEN.replace("v4", "v2")) + .unwrap_err(), + Error::TokenFormat + ); + + // Invalid purpose + assert_eq!( + UntrustedToken::<Public, V2>::try_from(&V2_PUBLIC_TOKEN.replace("public", "local")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V2>::try_from(&V2_LOCAL_TOKEN.replace("local", "public")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V3>::try_from(&V3_PUBLIC_TOKEN.replace("public", "local")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Local, V4>::try_from(&V4_LOCAL_TOKEN.replace("local", "public")) + .unwrap_err(), + Error::TokenFormat + ); + assert_eq!( + UntrustedToken::<Public, V4>::try_from(&V4_PUBLIC_TOKEN.replace("public", "local")) + .unwrap_err(), + Error::TokenFormat + ); + } + + #[test] + fn invalid_base64() { + for token in TOKEN_LIST { + let split = token.split('.').collect::<Vec<&str>>(); + + let invalid: String = format!("{}.{}.{}!.{}", split[0], split[1], split[2], split[3]); + test_untrusted_parse_fails(&invalid, Error::Base64); + + let invalid: String = format!("{}.{}.{}.{}!", split[0], split[1], split[2], split[3]); + test_untrusted_parse_fails(&invalid, Error::Base64); + } + } + + #[cfg(feature = "v2")] + #[test] + fn valid_v2_local() { + // "2-E-1" + let valid_no_footer = "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4PnW8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVODyfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ"; + // "2-E-5" + let valid_with_footer = "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + let untrusted_no_footer = UntrustedToken::<Local, V2>::try_from(valid_no_footer).unwrap(); + let untrusted_with_footer = + UntrustedToken::<Local, V2>::try_from(valid_with_footer).unwrap(); + + // Note: We don't test for untrusted message, since it is encrypted. + assert_eq!(untrusted_no_footer.untrusted_footer(), &[0u8; 0]); + assert_eq!( + untrusted_with_footer.untrusted_footer(), + "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}".as_bytes() + ); + } + + #[cfg(feature = "v2")] + #[test] + fn valid_v2_public() { + // "2-S-1" + let valid_no_footer = "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGntTu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_DjJK2ZXC2SUYuOFM-Q_5Cw"; + // "2-S-2" + let valid_with_footer = "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + let untrusted_no_footer = UntrustedToken::<Public, V2>::try_from(valid_no_footer).unwrap(); + let untrusted_with_footer = + UntrustedToken::<Public, V2>::try_from(valid_with_footer).unwrap(); + + assert_eq!( + untrusted_no_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!(untrusted_no_footer.untrusted_footer(), &[0u8; 0]); + + assert_eq!( + untrusted_with_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!( + untrusted_with_footer.untrusted_footer(), + "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}".as_bytes() + ); + } + + #[cfg(feature = "v3")] + #[test] + fn valid_v3_public() { + // "3-S-1" + let valid_no_footer = "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9qqEwwrKHKi5lJ7b9MBKc0G4MGZy0ptUiMv3lAUAaz-JY_zjoqBSIxMxhfAoeNYiSyvfUErj76KOPWm1OeNnBPkTSespeSXDGaDfxeIrl3bRrPEIy7tLwLAIsRzsXkfph"; + // "3-S-2" + let valid_with_footer = "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9"; + + let untrusted_no_footer = UntrustedToken::<Public, V3>::try_from(valid_no_footer).unwrap(); + let untrusted_with_footer = + UntrustedToken::<Public, V3>::try_from(valid_with_footer).unwrap(); + + assert_eq!( + untrusted_no_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!(untrusted_no_footer.untrusted_footer(), &[0u8; 0]); + + assert_eq!( + untrusted_with_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!( + untrusted_with_footer.untrusted_footer(), + "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}".as_bytes() + ); + } + + #[cfg(feature = "v4")] + #[test] + fn valid_v4_public() { + // "4-S-1" + let valid_no_footer = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA"; + // "4-S-2" + let valid_with_footer = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + let untrusted_no_footer = UntrustedToken::<Public, V4>::try_from(valid_no_footer).unwrap(); + let untrusted_with_footer = + UntrustedToken::<Public, V4>::try_from(valid_with_footer).unwrap(); + + assert_eq!( + untrusted_no_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!(untrusted_no_footer.untrusted_footer(), &[0u8; 0]); + + assert_eq!( + untrusted_with_footer.untrusted_payload(), + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}" + .as_bytes() + ); + assert_eq!( + untrusted_with_footer.untrusted_footer(), + "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}".as_bytes() + ); + } + + #[cfg(feature = "v4")] + #[test] + fn valid_v4_local() { + // "4-E-1" + let valid_no_footer = "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg"; + // "4-E-5" + let valid_with_footer = "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + let untrusted_no_footer = UntrustedToken::<Local, V4>::try_from(valid_no_footer).unwrap(); + let untrusted_with_footer = + UntrustedToken::<Local, V4>::try_from(valid_with_footer).unwrap(); + + // Note: We don't test for untrusted message, since it is encrypted. + assert_eq!(untrusted_no_footer.untrusted_footer(), &[0u8; 0]); + assert_eq!( + untrusted_with_footer.untrusted_footer(), + "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}".as_bytes() + ); + } + + #[test] + fn local_token_nonce_tag_no_payload_v4() { + assert!(UntrustedToken::<Local, V4>::try_from( + "v4.local.444444bbbbb444444444bbb444444bbb44444444444444888888888888888cJJbbb44444444", + ) + .is_err()); + } + + #[test] + fn local_token_nonce_tag_no_payload_v3() { + assert!(UntrustedToken::<Public, V3>::try_from( + "v3.local.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", + ).is_err()); + } + + #[test] + fn test_get_footer_from_trusted() { + let mut footer = Footer::default(); + footer.add_additional("t", "v").unwrap(); + let mut tt = TrustedToken::_new( + "v3.local.", + b"test msg", + footer.to_string().unwrap().as_bytes(), + b"", + ) + .unwrap(); + assert!(Footer::try_from(&tt).is_ok()); + tt.footer = Vec::<u8>::new(); + assert!(Footer::try_from(&tt).is_err()); + } + + #[test] + fn test_trusted_claims() { + let mut footer = Footer::default(); + footer.add_additional("t", "v").unwrap(); + let mut tt = TrustedToken::_new( + "v3.local.", + b"test msg", + footer.to_string().unwrap().as_bytes(), + b"", + ) + .unwrap(); + let claims = Claims::new().unwrap(); + tt.set_payload_claims(claims.clone()); + + assert_eq!(tt.payload_claims.unwrap(), claims); + } +} diff --git a/vendor/pasetors/src/version.rs b/vendor/pasetors/src/version.rs new file mode 100644 index 000000000..6e5962c6a --- /dev/null +++ b/vendor/pasetors/src/version.rs @@ -0,0 +1,37 @@ +use crate::errors::Error; + +pub(crate) mod private { + use super::Error; + + // Inside private module to prevent users from implementing this themself. + + /// A given version must implement validation logic in terms of both itself and the kind of key. + pub trait Version { + /// Size for a `local` key. + const LOCAL_KEY: usize; + /// Size for a secret `public` key. + const SECRET_KEY: usize; + /// Size for a public `public` key. + const PUBLIC_KEY: usize; + /// Size of the signature for a public token. + const PUBLIC_SIG: usize; + /// Size of the nonce for a local token. + const LOCAL_NONCE: usize; + /// Size of the authentication tag for a local token. + const LOCAL_TAG: usize; + /// Header for a public token for this version. + const PUBLIC_HEADER: &'static str; + /// Header for a local token for this version. + const LOCAL_HEADER: &'static str; + /// Size of a PASERK ID. + #[cfg(feature = "paserk")] + const PASERK_ID: usize; + + /// Validate bytes for a `local` key of a given version. + fn validate_local_key(key_bytes: &[u8]) -> Result<(), Error>; + /// Validate bytes for a secret `public` key of a given version. + fn validate_secret_key(key_bytes: &[u8]) -> Result<(), Error>; + /// Validate bytes for a public `local` key of a given version. + fn validate_public_key(key_bytes: &[u8]) -> Result<(), Error>; + } +} diff --git a/vendor/pasetors/src/version2.rs b/vendor/pasetors/src/version2.rs new file mode 100644 index 000000000..f2a5c9a75 --- /dev/null +++ b/vendor/pasetors/src/version2.rs @@ -0,0 +1,828 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "v2")))] + +use crate::common::{encode_b64, validate_footer_untrusted_token}; +use crate::errors::Error; +use crate::keys::{ + AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate, SymmetricKey, +}; +use crate::pae; +use crate::token::{Local, Public, TrustedToken, UntrustedToken}; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::marker::PhantomData; +use ed25519_compact::{KeyPair, PublicKey, SecretKey as SigningKey, Seed, Signature}; +use orion::hazardous::aead::xchacha20poly1305::*; +use orion::hazardous::mac::blake2b; +use orion::hazardous::mac::poly1305::POLY1305_OUTSIZE; +use orion::hazardous::stream::xchacha20::XCHACHA_NONCESIZE; +use subtle::ConstantTimeEq; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// Version 2 of the PASETO spec. +pub struct V2; + +impl Version for V2 { + const LOCAL_KEY: usize = 32; + const SECRET_KEY: usize = 32 + Self::PUBLIC_KEY; // Seed || PK + const PUBLIC_KEY: usize = 32; + const PUBLIC_SIG: usize = 64; + const LOCAL_NONCE: usize = 24; + const LOCAL_TAG: usize = 16; + const PUBLIC_HEADER: &'static str = "v2.public."; + const LOCAL_HEADER: &'static str = "v2.local."; + #[cfg(feature = "paserk")] + const PASERK_ID: usize = 44; + + fn validate_local_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::LOCAL_KEY { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_secret_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::SECRET_KEY { + return Err(Error::Key); + } + + let seed = Seed::from_slice(&key_bytes[..32]).map_err(|_| Error::Key)?; + let kp = KeyPair::from_seed(seed); + + if !bool::from(kp.pk.as_slice().ct_eq(&key_bytes[32..])) { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_public_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::PUBLIC_KEY { + return Err(Error::Key); + } + + Ok(()) + } +} + +impl TryFrom<&AsymmetricSecretKey<V2>> for AsymmetricPublicKey<V2> { + type Error = Error; + + fn try_from(value: &AsymmetricSecretKey<V2>) -> Result<Self, Self::Error> { + AsymmetricPublicKey::<V2>::from(&value.as_bytes()[32..]) + } +} + +impl Generate<AsymmetricKeyPair<V2>, V2> for AsymmetricKeyPair<V2> { + fn generate() -> Result<AsymmetricKeyPair<V2>, Error> { + let key_pair = KeyPair::generate(); + + let secret = AsymmetricSecretKey::<V2>::from(key_pair.sk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + let public = AsymmetricPublicKey::<V2>::from(key_pair.pk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + + Ok(Self { public, secret }) + } +} + +impl Generate<SymmetricKey<V2>, V2> for SymmetricKey<V2> { + fn generate() -> Result<SymmetricKey<V2>, Error> { + let mut rng_bytes = vec![0u8; V2::LOCAL_KEY]; + V2::validate_local_key(&rng_bytes)?; + getrandom::getrandom(&mut rng_bytes)?; + + Ok(Self { + bytes: rng_bytes, + phantom: PhantomData, + }) + } +} + +/// PASETO v2 public tokens. +pub struct PublicToken; + +impl PublicToken { + /// The header and purpose for the public token: `v2.public.`. + pub const HEADER: &'static str = "v2.public."; + + /// Create a public token. + pub fn sign( + secret_key: &AsymmetricSecretKey<V2>, + message: &[u8], + footer: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let sk = SigningKey::from_slice(secret_key.as_bytes()).map_err(|_| Error::Key)?; + let f = footer.unwrap_or(&[]); + let m2 = pae::pae(&[Self::HEADER.as_bytes(), message, f])?; + let sig = sk.sign(m2, None); + + let mut m_sig: Vec<u8> = Vec::from(message); + m_sig.extend_from_slice(sig.as_ref()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(m_sig)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Verify a public token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn verify( + public_key: &AsymmetricPublicKey<V2>, + token: &UntrustedToken<Public, V2>, + footer: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let sm = token.untrusted_message(); + let m = token.untrusted_payload(); + let s = sm[m.len()..m.len() + V2::PUBLIC_SIG].as_ref(); + + let m2 = pae::pae(&[Self::HEADER.as_bytes(), m, f])?; + let pk: PublicKey = PublicKey::from_slice(public_key.as_bytes()).map_err(|_| Error::Key)?; + + debug_assert!(s.len() == V2::PUBLIC_SIG); + // If the below fails, it is an invalid signature. + let sig = Signature::from_slice(s).map_err(|_| Error::TokenValidation)?; + + if pk.verify(m2, &sig).is_ok() { + TrustedToken::_new(Self::HEADER, m, f, &[]) + } else { + Err(Error::TokenValidation) + } + } +} + +/// PASETO v2 local tokens. +pub struct LocalToken; + +impl LocalToken { + /// The header and purpose for the local token: `v2.local.`. + pub const HEADER: &'static str = "v2.local."; + + /// Encrypt and authenticate a message using nonce_key_bytes to derive a nonce + /// using BLAKE2b. + pub(crate) fn encrypt_with_derived_nonce( + secret_key: &SymmetricKey<V2>, + nonce_key_bytes: &[u8], + message: &[u8], + footer: Option<&[u8]>, + ) -> Result<String, Error> { + debug_assert!(nonce_key_bytes.len() == XCHACHA_NONCESIZE); + + // Safe unwrap()s due to lengths. + let nonce_key = blake2b::SecretKey::from_slice(nonce_key_bytes).unwrap(); + let mut blake2b = blake2b::Blake2b::new(&nonce_key, XCHACHA_NONCESIZE).unwrap(); + blake2b.update(message.as_ref()).unwrap(); + let nonce = Nonce::from_slice(blake2b.finalize().unwrap().unprotected_as_bytes()).unwrap(); + + let f = footer.unwrap_or(&[]); + + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), nonce.as_ref(), f])?; + let mut out = vec![0u8; message.len() + POLY1305_OUTSIZE + nonce.len()]; + let sk = match SecretKey::from_slice(secret_key.as_bytes()) { + Ok(val) => val, + Err(orion::errors::UnknownCryptoError) => return Err(Error::Key), + }; + + match seal( + &sk, + &nonce, + message, + Some(&pre_auth), + &mut out[nonce.len()..], + ) { + Ok(()) => (), + Err(orion::errors::UnknownCryptoError) => return Err(Error::Encryption), + } + + out[..nonce.len()].copy_from_slice(nonce.as_ref()); + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(out)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Create a local token. + pub fn encrypt( + secret_key: &SymmetricKey<V2>, + message: &[u8], + footer: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let mut rng_bytes = [0u8; XCHACHA_NONCESIZE]; + getrandom::getrandom(&mut rng_bytes)?; + + Self::encrypt_with_derived_nonce(secret_key, &rng_bytes, message, footer) + } + + /// Verify and decrypt a local token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn decrypt( + secret_key: &SymmetricKey<V2>, + token: &UntrustedToken<Local, V2>, + footer: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let nc = token.untrusted_message(); + let n = nc[..XCHACHA_NONCESIZE].as_ref(); + let c = nc[n.len()..].as_ref(); + + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), n, f])?; + let mut out = vec![0u8; c.len() - POLY1305_OUTSIZE]; + + let sk = match SecretKey::from_slice(secret_key.as_bytes()) { + Ok(val) => val, + Err(orion::errors::UnknownCryptoError) => return Err(Error::Key), + }; + + match open( + &sk, + &Nonce::from_slice(n).unwrap(), + c, + Some(pre_auth.as_ref()), + &mut out, + ) { + Ok(()) => TrustedToken::_new(Self::HEADER, &out, f, &[]), + Err(orion::errors::UnknownCryptoError) => Err(Error::TokenValidation), + } + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod test_vectors { + + use hex; + + use super::*; + use core::convert::TryFrom; + use std::fs::File; + use std::io::BufReader; + + use crate::claims::Claims; + use crate::common::tests::*; + + fn test_local(test: &PasetoTest) { + debug_assert!(test.nonce.is_some()); + debug_assert!(test.key.is_some()); + + let sk = + SymmetricKey::<V2>::from(&hex::decode(test.key.as_ref().unwrap()).unwrap()).unwrap(); + + let nonce = hex::decode(test.nonce.as_ref().unwrap()).unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Local, V2>::try_from(&test.token) { + assert!(LocalToken::decrypt(&sk, &ut, footer).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = + LocalToken::encrypt_with_derived_nonce(&sk, &nonce, message.as_bytes(), footer) + .unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + + let ut = UntrustedToken::<Local, V2>::try_from(&test.token).unwrap(); + let trusted = LocalToken::decrypt(&sk, &ut, footer).unwrap(); + assert_eq!(trusted.payload(), message, "Failed {:?}", test.name); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), LocalToken::HEADER); + assert!(trusted.implicit_assert().is_empty()); + + let parsed_claims = Claims::from_bytes(trusted.payload().as_bytes()).unwrap(); + let test_vector_claims = serde_json::from_str::<Payload>(message).unwrap(); + + assert_eq!( + parsed_claims.get_claim("data").unwrap().as_str().unwrap(), + test_vector_claims.data, + ); + assert_eq!( + parsed_claims.get_claim("exp").unwrap().as_str().unwrap(), + test_vector_claims.exp, + ); + } + + fn test_public(test: &PasetoTest) { + debug_assert!(test.public_key.is_some()); + debug_assert!(test.secret_key.is_some()); + + let sk = AsymmetricSecretKey::<V2>::from( + &hex::decode(test.secret_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let pk = AsymmetricPublicKey::<V2>::from( + &hex::decode(test.public_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Public, V2>::try_from(&test.token) { + assert!(PublicToken::verify(&pk, &ut, footer).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = PublicToken::sign(&sk, message.as_bytes(), footer).unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + let ut = UntrustedToken::<Public, V2>::try_from(&test.token).unwrap(); + + let trusted = PublicToken::verify(&pk, &ut, footer).unwrap(); + assert_eq!(trusted.payload(), message); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), PublicToken::HEADER); + assert!(trusted.implicit_assert().is_empty()); + } + + #[test] + fn run_test_vectors() { + let path = "./test_vectors/v2.json"; + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for t in tests.tests { + // v2.public + if t.public_key.is_some() { + test_public(&t); + } + // v2.local + if t.nonce.is_some() { + test_local(&t); + } + } + } +} + +#[cfg(test)] +mod test_tokens { + use super::*; + use crate::common::decode_b64; + use crate::keys::{AsymmetricKeyPair, Generate}; + use crate::token::UntrustedToken; + use core::convert::TryFrom; + + const TEST_LOCAL_SK_BYTES: [u8; 32] = [ + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, + 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + ]; + + pub(crate) const TEST_SK_BYTES: [u8; 64] = [ + 180, 203, 251, 67, 223, 76, 226, 16, 114, 125, 149, 62, 74, 113, 51, 7, 250, 25, 187, 125, + 159, 133, 4, 20, 56, 217, 225, 27, 148, 42, 55, 116, 30, 185, 219, 187, 188, 4, 124, 3, + 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, 117, 114, 37, 193, 31, 0, 65, 93, + 14, 32, 177, 162, + ]; + + const TEST_PK_BYTES: [u8; 32] = [ + 30, 185, 219, 187, 188, 4, 124, 3, 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, + 117, 114, 37, 193, 31, 0, 65, 93, 14, 32, 177, 162, + ]; + + const MESSAGE: &str = + "{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + const FOOTER: &str = "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"; + const VALID_PUBLIC_TOKEN: &str = "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYCR0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const VALID_LOCAL_TOKEN: &str = "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bbjo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + #[test] + fn test_gen_keypair() { + let kp = AsymmetricKeyPair::<V2>::generate().unwrap(); + + let token = PublicToken::sign(&kp.secret, MESSAGE.as_bytes(), None).unwrap(); + + let ut = UntrustedToken::<Public, V2>::try_from(&token).unwrap(); + assert!(PublicToken::verify(&kp.public, &ut, None).is_ok()); + } + + #[test] + fn test_untrusted_token_usage() { + // Local + let sk = SymmetricKey::<V2>::generate().unwrap(); + let token = LocalToken::encrypt(&sk, MESSAGE.as_bytes(), Some(FOOTER.as_bytes())).unwrap(); + + let untrusted_token = UntrustedToken::<Local, V2>::try_from(token.as_str()).unwrap(); + let _ = LocalToken::decrypt( + &sk, + &untrusted_token, + Some(untrusted_token.untrusted_footer()), + ) + .unwrap(); + + // Public + let kp = AsymmetricKeyPair::<V2>::generate().unwrap(); + let token = + PublicToken::sign(&kp.secret, MESSAGE.as_bytes(), Some(FOOTER.as_bytes())).unwrap(); + + let untrusted_token = UntrustedToken::<Public, V2>::try_from(token.as_str()).unwrap(); + assert!(PublicToken::verify(&kp.public, &untrusted_token, Some(FOOTER.as_bytes())).is_ok()); + } + + #[test] + fn test_roundtrip_local() { + let sk = SymmetricKey::<V2>::generate().unwrap(); + let message = "token payload"; + + let token = LocalToken::encrypt(&sk, message.as_bytes(), None).unwrap(); + let ut = UntrustedToken::<Local, V2>::try_from(&token).unwrap(); + let trusted_token = LocalToken::decrypt(&sk, &ut, None).unwrap(); + + assert_eq!(trusted_token.payload(), message); + } + + #[test] + fn test_roundtrip_public() { + let test_sk = AsymmetricSecretKey::<V2>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + + let token = PublicToken::sign(&test_sk, MESSAGE.as_bytes(), None).unwrap(); + let ut = UntrustedToken::<Public, V2>::try_from(&token).unwrap(); + + assert!(PublicToken::verify(&test_pk, &ut, None).is_ok()); + } + + #[test] + fn footer_logic() { + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V2>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + + // We create a token with Some(footer) and with None + let actual_some = UntrustedToken::<Public, V2>::try_from( + &PublicToken::sign(&test_sk, message, Some(FOOTER.as_bytes())).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V2>::try_from( + &PublicToken::sign(&test_sk, message, None).unwrap(), + ) + .unwrap(); + + // token = Some(footer) = validate and compare + // token = None(footer) = validate only + + // We should be able to validate with None if created with Some() (excludes constant-time + // comparison with known value) + assert!(PublicToken::verify(&test_pk, &actual_some, None).is_ok()); + // We should be able to validate with Some() if created with Some() + assert!(PublicToken::verify(&test_pk, &actual_some, Some(FOOTER.as_bytes())).is_ok()); + // We should NOT be able to validate with Some() if created with None + assert!(PublicToken::verify(&test_pk, &actual_none, Some(FOOTER.as_bytes())).is_err()); + + let actual_some = UntrustedToken::<Local, V2>::try_from( + &LocalToken::encrypt(&test_local_sk, message, Some(FOOTER.as_bytes())).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Local, V2>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None).unwrap(), + ) + .unwrap(); + + // They don't equal because the nonce is random. So we only check decryption. + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, None).is_ok()); + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, Some(FOOTER.as_bytes())).is_ok()); + assert!( + LocalToken::decrypt(&test_local_sk, &actual_none, Some(FOOTER.as_bytes())).is_err() + ); + } + + #[test] + // NOTE: See https://github.com/paseto-standard/paseto-spec/issues/17 + fn empty_payload() { + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V2>::from(&TEST_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::sign(&test_sk, b"", None).unwrap_err(), + Error::EmptyPayload + ); + assert_eq!( + LocalToken::encrypt(&test_local_sk, b"", None).unwrap_err(), + Error::EmptyPayload + ); + } + + #[test] + fn err_on_modified_footer() { + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V2>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_footer_in_token_none_supplied() { + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V2>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(b"") + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(b"") + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_no_footer_in_token_some_supplied() { + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_public: String = format!( + "{}.{}.{}", + split_public[0], split_public[1], split_public[2] + ); + + let split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_local: String = + format!("{}.{}.{}", split_local[0], split_local[1], split_local[2]); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V2>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_signature() { + let test_pk = AsymmetricPublicKey::<V2>::from(&TEST_PK_BYTES).unwrap(); + + let mut split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_sig = decode_b64(split_public[2]).unwrap(); + bad_sig.copy_within(0..32, 32); + let tmp = encode_b64(bad_sig).unwrap(); + split_public[2] = &tmp; + let invalid_public: String = format!( + "{}.{}.{}.{}", + split_public[0], split_public[1], split_public[2], split_public[3] + ); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V2>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_tag() { + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_tag = decode_b64(split_local[2]).unwrap(); + let tlen = bad_tag.len(); + bad_tag.copy_within(0..16, tlen - 16); + let tmp = encode_b64(bad_tag).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_ciphertext() { + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_ct = decode_b64(split_local[2]).unwrap(); + let ctlen = bad_ct.len(); + bad_ct.copy_within((ctlen - 16)..ctlen, 24); + let tmp = encode_b64(bad_ct).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_nonce() { + let test_local_sk = SymmetricKey::<V2>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_nonce = decode_b64(split_local[2]).unwrap(); + let nlen = bad_nonce.len(); + bad_nonce.copy_within((nlen - 24)..nlen, 0); + let tmp = encode_b64(bad_nonce).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V2>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_public_secret_key() { + let bad_pk = AsymmetricPublicKey::<V2>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + PublicToken::verify( + &bad_pk, + &UntrustedToken::<Public, V2>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_shared_secret_key() { + let bad_local_sk = SymmetricKey::<V2>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + LocalToken::decrypt( + &bad_local_sk, + &UntrustedToken::<Local, V2>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()) + ) + .unwrap_err(), + Error::TokenValidation + ); + } +} + +#[cfg(test)] +mod test_keys { + use super::*; + use crate::version2::test_tokens::TEST_SK_BYTES; + + #[test] + fn test_symmetric_gen() { + let randomv = SymmetricKey::<V2>::generate().unwrap(); + assert_ne!(randomv.as_bytes(), &[0u8; 32]); + } + + #[test] + fn test_invalid_sizes() { + assert!(AsymmetricSecretKey::<V2>::from(&[1u8; 63]).is_err()); + assert!(AsymmetricSecretKey::<V2>::from(&TEST_SK_BYTES).is_ok()); + assert!(AsymmetricSecretKey::<V2>::from(&[1u8; 65]).is_err()); + + assert!(AsymmetricPublicKey::<V2>::from(&[1u8; 31]).is_err()); + assert!(AsymmetricPublicKey::<V2>::from(&[1u8; 32]).is_ok()); + assert!(AsymmetricPublicKey::<V2>::from(&[1u8; 33]).is_err()); + + assert!(SymmetricKey::<V2>::from(&[0u8; 31]).is_err()); + assert!(SymmetricKey::<V2>::from(&[0u8; 32]).is_ok()); + assert!(SymmetricKey::<V2>::from(&[0u8; 33]).is_err()); + } + + #[test] + fn try_from_secret_to_public() { + let kpv2 = AsymmetricKeyPair::<V2>::generate().unwrap(); + let pubv2 = AsymmetricPublicKey::<V2>::try_from(&kpv2.secret).unwrap(); + assert_eq!(pubv2.as_bytes(), kpv2.public.as_bytes()); + assert_eq!(pubv2, kpv2.public); + assert_eq!(&kpv2.secret.as_bytes()[32..], pubv2.as_bytes()); + } + + #[test] + fn test_trait_impls() { + let debug = format!("{:?}", SymmetricKey::<V2>::generate().unwrap()); + assert_eq!(debug, "SymmetricKey {***OMITTED***}"); + + let randomv = SymmetricKey::<V2>::generate().unwrap(); + let zero = SymmetricKey::<V2>::from(&[0u8; V2::LOCAL_KEY]).unwrap(); + assert_ne!(randomv, zero); + + let debug = format!("{:?}", AsymmetricKeyPair::<V2>::generate().unwrap().secret); + assert_eq!(debug, "AsymmetricSecretKey {***OMITTED***}"); + + let random1 = AsymmetricKeyPair::<V2>::generate().unwrap(); + let random2 = AsymmetricKeyPair::<V2>::generate().unwrap(); + assert_ne!(random1.secret, random2.secret); + } + + #[test] + fn test_clone() { + let sk = SymmetricKey::<V2>::generate().unwrap(); + assert_eq!(sk, sk.clone()); + + let kp = AsymmetricKeyPair::<V2>::generate().unwrap(); + assert_eq!(kp.secret, kp.secret.clone()); + assert_eq!(kp.public, kp.public.clone()); + } +} diff --git a/vendor/pasetors/src/version3.rs b/vendor/pasetors/src/version3.rs new file mode 100644 index 000000000..5c84a689a --- /dev/null +++ b/vendor/pasetors/src/version3.rs @@ -0,0 +1,817 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "v3")))] + +//! +//! This is an implementation of the [version 3 specification of PASETO](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md#sign). +//! +//! The following points apply to this implementation, in regards to the specification: +//! - PASETO requires the use of compressed public keys. If these are not readily supported in a given +//! setting, [UncompressedPublicKey] and [AsymmetricPublicKey<V3>] conversions can be used to obtain +//! the compressed form. +//! - PASETO recommends use of deterministic nonces ([RFC 6979]) which this library also uses. +//! - Hedged signatures, according to the PASETO spec, are not used. +//! +//! [AsymmetricPublicKey<V3>]: crate::keys::AsymmetricPublicKey +//! [UncompressedPublicKey]: crate::version3::UncompressedPublicKey +//! [RFC 6979]: https://tools.ietf.org/html/rfc6979 + +use core::marker::PhantomData; + +use crate::common::{encode_b64, validate_footer_untrusted_token}; +use crate::errors::Error; +use crate::keys::{AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate}; +use crate::pae; +use crate::token::{Public, TrustedToken, UntrustedToken}; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryFrom; +use p384::ecdsa::{ + signature::DigestSigner, signature::DigestVerifier, Signature, SigningKey, VerifyingKey, +}; +use p384::elliptic_curve::sec1::ToEncodedPoint; +use p384::PublicKey; +use rand_core::OsRng; +use sha2::Digest; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// Version 3 of the PASETO spec. +pub struct V3; + +impl Version for V3 { + const LOCAL_KEY: usize = 32; + const SECRET_KEY: usize = 48; + const PUBLIC_KEY: usize = 49; + const PUBLIC_SIG: usize = 96; + const LOCAL_NONCE: usize = 32; + const LOCAL_TAG: usize = 48; + const PUBLIC_HEADER: &'static str = "v3.public."; + const LOCAL_HEADER: &'static str = "v3.local."; + #[cfg(feature = "paserk")] + const PASERK_ID: usize = 44; + + fn validate_local_key(_key_bytes: &[u8]) -> Result<(), Error> { + unimplemented!(); + } + + fn validate_secret_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::SECRET_KEY { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_public_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::PUBLIC_KEY { + return Err(Error::Key); + } + if key_bytes[0] != 0x02 && key_bytes[0] != 0x03 { + return Err(Error::Key); + } + + Ok(()) + } +} + +impl TryFrom<&AsymmetricSecretKey<V3>> for AsymmetricPublicKey<V3> { + type Error = Error; + + fn try_from(value: &AsymmetricSecretKey<V3>) -> Result<Self, Self::Error> { + let sk = SigningKey::from_bytes(value.as_bytes()).map_err(|_| Error::Key)?; + AsymmetricPublicKey::<V3>::from(sk.verifying_key().to_encoded_point(true).as_bytes()) + } +} + +impl Generate<AsymmetricKeyPair<V3>, V3> for AsymmetricKeyPair<V3> { + fn generate() -> Result<AsymmetricKeyPair<V3>, Error> { + let key = SigningKey::random(&mut OsRng); + + let public = AsymmetricPublicKey::<V3>::from( + VerifyingKey::from(&key).to_encoded_point(true).as_ref(), + )?; + let secret = AsymmetricSecretKey::<V3>::from(key.to_bytes().as_slice())?; + + Ok(Self { public, secret }) + } +} + +/// This struct represents a uncompressed public key for P384, encoded in big-endian using: +/// Octet-String-to-Elliptic-Curve-Point algorithm in SEC 1: Elliptic Curve Cryptography, Version 2.0. +/// +/// Format: `[0x04 || x || y]` +/// +/// This is provided to be able to convert uncompressed keys to compressed ones, as compressed is +/// required by PASETO and what an [`AsymmetricPublicKey<V3>`] represents. +pub struct UncompressedPublicKey(PublicKey); + +impl TryFrom<&[u8]> for UncompressedPublicKey { + type Error = Error; + + fn try_from(value: &[u8]) -> Result<Self, Self::Error> { + // PublicKey::from_sec1_bytes accepts both uncompressed and compressed points + // but we need to make the distiction here. + if value.len() != 97 && value[0] != 4 { + return Err(Error::Key); + } + + let pk = PublicKey::from_sec1_bytes(value).map_err(|_| Error::Key)?; + + Ok(Self(pk)) + } +} + +impl TryFrom<&AsymmetricPublicKey<V3>> for UncompressedPublicKey { + type Error = Error; + + fn try_from(value: &AsymmetricPublicKey<V3>) -> Result<Self, Self::Error> { + // PublicKey::from_sec1_bytes accepts both uncompressed and compressed points + // but we need to make the distiction here. + if value.as_bytes()[0] != 2 && value.as_bytes()[0] != 3 { + return Err(Error::Key); + } + + let pk = PublicKey::from_sec1_bytes(value.as_bytes()).map_err(|_| Error::Key)?; + + Ok(UncompressedPublicKey(pk)) + } +} + +impl TryFrom<&UncompressedPublicKey> for AsymmetricPublicKey<V3> { + type Error = Error; + + fn try_from(value: &UncompressedPublicKey) -> Result<Self, Self::Error> { + Ok(Self { + bytes: value.0.to_encoded_point(true).as_ref().to_vec(), + phantom: PhantomData, + }) + } +} + +/// PASETO v3 public tokens. +pub struct PublicToken; + +impl PublicToken { + /// The header and purpose for the public token: `v3.public.`. + pub const HEADER: &'static str = "v3.public."; + + /// Create a public token. + /// + /// The `secret_key` **must** be in big-endian. + pub fn sign( + secret_key: &AsymmetricSecretKey<V3>, + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let signing_key = SigningKey::from_bytes(secret_key.as_bytes()).map_err(|_| Error::Key)?; + let public_key = VerifyingKey::from(&signing_key).to_encoded_point(true); + + let f = footer.unwrap_or(&[]); + let i = implicit_assert.unwrap_or(&[]); + let m2 = pae::pae(&[public_key.as_ref(), Self::HEADER.as_bytes(), message, f, i])?; + + let mut msg_digest = sha2::Sha384::new(); + msg_digest.update(m2); + + let sig = signing_key + .try_sign_digest(msg_digest) + .map_err(|_| Error::Signing)?; + debug_assert_eq!(sig.as_ref().len(), V3::PUBLIC_SIG); + + let mut m_sig: Vec<u8> = Vec::from(message); + m_sig.extend_from_slice(sig.as_ref()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(m_sig)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Verify a public token. + /// + /// The `public_key` **must** be in big-endian. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn verify( + public_key: &AsymmetricPublicKey<V3>, + token: &UntrustedToken<Public, V3>, + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let i = implicit_assert.unwrap_or(&[]); + let sm = token.untrusted_message(); + let m = token.untrusted_payload(); + let s = Signature::try_from(sm[m.len()..m.len() + V3::PUBLIC_SIG].as_ref()) + .map_err(|_| Error::TokenValidation)?; + + let m2 = pae::pae(&[public_key.as_bytes(), Self::HEADER.as_bytes(), m, f, i])?; + + let verifying_key = + VerifyingKey::from_sec1_bytes(public_key.as_bytes()).map_err(|_| Error::Key)?; + + let mut msg_digest = sha2::Sha384::new(); + msg_digest.update(m2); + verifying_key + .verify_digest(msg_digest, &s) + .map_err(|_| Error::TokenValidation)?; + + TrustedToken::_new(Self::HEADER, m, f, i) + } +} + +#[cfg(test)] +mod test_regression { + use super::*; + use crate::keys::AsymmetricPublicKey; + use core::convert::TryFrom; + use p384::elliptic_curve::sec1::ToEncodedPoint; + + #[test] + fn fuzzer_regression_1() { + let pk_bytes: [u8; 97] = [ + 4, 0, 205, 193, 144, 253, 175, 61, 67, 178, 31, 65, 80, 197, 219, 197, 12, 136, 239, + 15, 12, 155, 112, 129, 17, 35, 64, 33, 149, 251, 222, 174, 69, 197, 171, 176, 115, 67, + 144, 76, 135, 147, 21, 48, 196, 235, 169, 93, 34, 100, 63, 20, 128, 61, 191, 214, 161, + 240, 38, 228, 74, 250, 91, 185, 68, 243, 172, 203, 43, 174, 99, 230, 231, 239, 161, 78, + 148, 160, 170, 87, 200, 24, 220, 196, 53, 107, 22, 85, 59, 227, 237, 150, 83, 81, 41, + 2, 132, + ]; + + let uc_pk = UncompressedPublicKey::try_from(pk_bytes.as_ref()).unwrap(); + assert_eq!(&pk_bytes, &uc_pk.0.to_encoded_point(false).as_ref()); + let c_pk = AsymmetricPublicKey::<V3>::try_from(&uc_pk).unwrap(); + assert_eq!(&c_pk.as_bytes()[1..], &pk_bytes[1..49]); + + let round = UncompressedPublicKey::try_from(&c_pk).unwrap(); + + assert_eq!(round.0.to_encoded_point(false).as_ref(), pk_bytes); + } + + #[test] + fn fuzzer_regression_2() { + let data: [u8; 49] = [ + 2, 0, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, + 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, + 49, 49, 49, 49, 49, + ]; + + if let Ok(compressed_pk) = AsymmetricPublicKey::<V3>::from(&data) { + if let Ok(uncompressed) = UncompressedPublicKey::try_from(&compressed_pk) { + assert_eq!( + AsymmetricPublicKey::<V3>::try_from(&uncompressed) + .unwrap() + .as_bytes(), + compressed_pk.as_bytes() + ); + } + } + } + + #[test] + fn fuzzer_regression_3() { + let data: [u8; 49] = [ + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ]; + + if let Ok(compressed_pk) = AsymmetricPublicKey::<V3>::from(&data) { + if let Ok(uncompressed) = UncompressedPublicKey::try_from(&compressed_pk) { + assert_eq!( + AsymmetricPublicKey::<V3>::try_from(&uncompressed) + .unwrap() + .as_bytes(), + compressed_pk.as_bytes() + ); + } + } + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod test_vectors { + + use super::*; + use hex; + use std::fs::File; + use std::io::BufReader; + + use crate::common::tests::*; + + fn test_pk_conversion(pk: &AsymmetricPublicKey<V3>) { + let uc_pk = UncompressedPublicKey::try_from(pk).unwrap(); + let c_pk: AsymmetricPublicKey<V3> = AsymmetricPublicKey::try_from(&uc_pk).unwrap(); + + assert_eq!( + pk.as_bytes(), + c_pk.as_bytes(), + "Failed to roundtrip conversion between compressed and uncompressed public key" + ); + } + + #[test] + fn sign_verify_roundtrip() { + // Values taken from 3-S-1 + let raw_sk = hex::decode("20347609607477aca8fbfbc5e6218455f3199669792ef8b466faa87bdc67798144c848dd03661eed5ac62461340cea96").unwrap(); + let raw_pk = hex::decode("02fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb").unwrap(); + + let sk = AsymmetricSecretKey::<V3>::from(&raw_sk).unwrap(); + let pk = AsymmetricPublicKey::<V3>::from(&raw_pk).unwrap(); + let message = "this is a signed message"; + + let token = UntrustedToken::<Public, V3>::try_from( + &PublicToken::sign(&sk, message.as_bytes(), Some(b"footer"), Some(b"impl")).unwrap(), + ) + .unwrap(); + assert!(PublicToken::verify(&pk, &token, Some(b"footer"), Some(b"impl")).is_ok()); + } + + fn test_public(test: &PasetoTest) { + debug_assert!(test.public_key.is_some()); + debug_assert!(test.secret_key.is_some()); + + let sk = AsymmetricSecretKey::<V3>::from( + &hex::decode(test.secret_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let pk = AsymmetricPublicKey::<V3>::from( + &hex::decode(test.public_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + + test_pk_conversion(&pk); + + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + let implicit_assert = test.implicit_assertion.as_bytes(); + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Public, V3>::try_from(&test.token) { + assert!(PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + let actual = + PublicToken::sign(&sk, message.as_bytes(), footer, Some(implicit_assert)).unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + let ut = UntrustedToken::<Public, V3>::try_from(&test.token).unwrap(); + + let trusted = PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).unwrap(); + assert_eq!(trusted.payload(), message); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), PublicToken::HEADER); + assert_eq!(trusted.implicit_assert(), implicit_assert); + } + + #[test] + fn run_test_vectors() { + let path = "./test_vectors/v3.json"; + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for t in tests.tests { + // v3.public + if t.public_key.is_some() { + test_public(&t); + } + } + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod test_wycheproof_point_compression { + use super::*; + use crate::keys::AsymmetricPublicKey; + use alloc::string::String; + use alloc::vec::Vec; + use p384::elliptic_curve::sec1::ToEncodedPoint; + use serde::{Deserialize, Serialize}; + use std::convert::TryFrom; + use std::fs::File; + use std::io::BufReader; + + #[allow(dead_code)] // `notes` field + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct WycheproofSecp384r1Tests { + algorithm: String, + generatorVersion: String, + numberOfTests: u64, + header: Vec<String>, + #[serde(skip)] + notes: Vec<String>, // Not a Vec<>, but we don't need this so skip it. + schema: String, + testGroups: Vec<Secp384r1TestGroup>, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct Secp384r1TestGroup { + key: Secp384r1Key, + keyDer: String, + keyPem: String, + sha: String, + #[serde(rename(deserialize = "type"))] + testType: String, + tests: Vec<TestVector>, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct Secp384r1Key { + curve: String, + keySize: u64, + #[serde(rename(deserialize = "type"))] + keyType: String, + uncompressed: String, + wx: String, + wy: String, + } + + #[allow(non_snake_case)] + #[derive(Serialize, Deserialize, Debug)] + pub(crate) struct TestVector { + tcId: u64, + comment: String, + msg: String, + sig: String, + result: String, + flags: Vec<String>, + } + + fn wycheproof_point_compression(path: &str) { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let tests: WycheproofSecp384r1Tests = serde_json::from_reader(reader).unwrap(); + + for test_group in tests.testGroups.iter() { + let uc_pk = UncompressedPublicKey::try_from( + hex::decode(&test_group.key.uncompressed) + .unwrap() + .as_slice(), + ) + .expect("Failed Wycheproof -> Uncompressed"); + + let pk = AsymmetricPublicKey::<V3>::try_from(&uc_pk).unwrap(); + assert_eq!( + hex::encode( + UncompressedPublicKey::try_from(&pk) + .unwrap() + .0 + .to_encoded_point(false) + .as_ref() + ), + test_group.key.uncompressed, + "Failed {:?}", + &test_group.key.uncompressed + ); + } + } + + #[test] + fn run_wycheproof_points() { + wycheproof_point_compression( + "./test_vectors/wycheproof/ecdsa_secp384r1_sha3_384_test.json", + ); + wycheproof_point_compression("./test_vectors/wycheproof/ecdsa_secp384r1_sha384_test.json"); + } +} + +#[cfg(test)] +mod test_tokens { + use super::*; + use crate::common::decode_b64; + use crate::keys::{AsymmetricKeyPair, Generate}; + use crate::token::UntrustedToken; + + // 3-S-2 values + const TEST_SK_BYTES: [u8; 48] = [ + 32, 52, 118, 9, 96, 116, 119, 172, 168, 251, 251, 197, 230, 33, 132, 85, 243, 25, 150, 105, + 121, 46, 248, 180, 102, 250, 168, 123, 220, 103, 121, 129, 68, 200, 72, 221, 3, 102, 30, + 237, 90, 198, 36, 97, 52, 12, 234, 150, + ]; + const TEST_PK_BYTES: [u8; 49] = [ + 2, 251, 203, 124, 105, 238, 28, 96, 87, 155, 231, 163, 52, 19, 72, 120, 217, 197, 197, 191, + 53, 213, 82, 218, 182, 60, 1, 64, 57, 126, 209, 76, 239, 99, 125, 119, 32, 146, 92, 68, + 105, 158, 163, 14, 114, 135, 76, 114, 251, + ]; + + const MESSAGE: &str = + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"; + const FOOTER: &str = "{\"kid\":\"dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn\"}"; + const VALID_PUBLIC_TOKEN: &str = "v3.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9ZWrbGZ6L0MDK72skosUaS0Dz7wJ_2bMcM6tOxFuCasO9GhwHrvvchqgXQNLQQyWzGC2wkr-VKII71AvkLpC8tJOrzJV1cap9NRwoFzbcXjzMZyxQ0wkshxZxx8ImmNWP.eyJraWQiOiJkWWtJU3lseFFlZWNFY0hFTGZ6Rjg4VVpyd2JMb2xOaUNkcHpVSEd3OVVxbiJ9"; + + #[test] + fn test_gen_keypair() { + let kp = AsymmetricKeyPair::<V3>::generate().unwrap(); + + let token = PublicToken::sign(&kp.secret, MESSAGE.as_bytes(), None, None).unwrap(); + + let ut = UntrustedToken::<Public, V3>::try_from(&token).unwrap(); + assert!(PublicToken::verify(&kp.public, &ut, None, None).is_ok()); + } + + #[test] + fn test_untrusted_token_usage() { + // Public + let kp = AsymmetricKeyPair::<V3>::generate().unwrap(); + let token = PublicToken::sign( + &kp.secret, + MESSAGE.as_bytes(), + Some(FOOTER.as_bytes()), + None, + ) + .unwrap(); + + let untrusted_token = UntrustedToken::<Public, V3>::try_from(token.as_str()).unwrap(); + assert!(PublicToken::verify( + &kp.public, + &untrusted_token, + Some(untrusted_token.untrusted_footer()), + None + ) + .is_ok()); + } + + #[test] + fn test_roundtrip_public() { + let test_sk = AsymmetricSecretKey::<V3>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + + let token = PublicToken::sign(&test_sk, MESSAGE.as_bytes(), None, None).unwrap(); + let ut = UntrustedToken::<Public, V3>::try_from(&token).unwrap(); + + assert!(PublicToken::verify(&test_pk, &ut, None, None).is_ok()); + } + + #[test] + fn footer_logic() { + let test_sk = AsymmetricSecretKey::<V3>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + + // We create a token with Some(footer) and with None + let actual_some = UntrustedToken::<Public, V3>::try_from( + &PublicToken::sign(&test_sk, message, Some(FOOTER.as_bytes()), None).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V3>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + + // token = Some(footer) = validate and compare + // token = None(footer) = validate only + + // We should be able to validate with None if created with Some() (excludes constant-time + // comparison with known value) + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + // We should be able to validate with Some() if created with Some() + assert!(PublicToken::verify(&test_pk, &actual_some, Some(FOOTER.as_bytes()), None).is_ok()); + // We should NOT be able to validate with Some() if created with None + assert!( + PublicToken::verify(&test_pk, &actual_none, Some(FOOTER.as_bytes()), None).is_err() + ); + } + + #[test] + fn implicit_none_some_empty_is_same() { + let test_sk = AsymmetricSecretKey::<V3>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + let implicit = b""; + + let actual_some = UntrustedToken::<Public, V3>::try_from( + &PublicToken::sign(&test_sk, message, None, Some(implicit)).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V3>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + + assert!(PublicToken::verify(&test_pk, &actual_none, None, Some(implicit)).is_ok()); + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + } + + #[test] + // NOTE: See https://github.com/paseto-standard/paseto-spec/issues/17 + fn empty_payload() { + let test_sk = AsymmetricSecretKey::<V3>::from(&TEST_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::sign(&test_sk, b"", None, None).unwrap_err(), + Error::EmptyPayload + ); + } + + #[test] + fn err_on_modified_footer() { + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_wrong_implicit_assert() { + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + assert!(PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .is_ok()); + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + Some(b"WRONG IMPLICIT") + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_footer_in_token_none_supplied() { + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(b""), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_no_footer_in_token_some_supplied() { + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + + let split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_public: String = format!( + "{}.{}.{}", + split_public[0], split_public[1], split_public[2] + ); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_signature() { + let test_pk = AsymmetricPublicKey::<V3>::from(&TEST_PK_BYTES).unwrap(); + + let mut split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_sig = decode_b64(split_public[2]).unwrap(); + bad_sig.copy_within(0..32, 32); + let tmp = encode_b64(bad_sig).unwrap(); + split_public[2] = &tmp; + let invalid_public: String = format!( + "{}.{}.{}.{}", + split_public[0], split_public[1], split_public[2], split_public[3] + ); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V3>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_public_secret_key() { + let mut pk_bytes = [0u8; 49]; + pk_bytes[0] = 2; + let bad_pk = AsymmetricPublicKey::<V3>::from(&pk_bytes).unwrap(); + + assert_eq!( + PublicToken::verify( + &bad_pk, + &UntrustedToken::<Public, V3>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } +} + +#[cfg(test)] +mod test_keys { + use super::*; + use crate::keys::SymmetricKey; + + #[test] + #[should_panic] + fn test_v3_local_not_implemented() { + assert!(SymmetricKey::<V3>::from(&[0u8; 32]).is_ok()); + } + + #[test] + fn test_invalid_sizes() { + assert!(AsymmetricSecretKey::<V3>::from(&[0u8; 47]).is_err()); + assert!(AsymmetricSecretKey::<V3>::from(&[0u8; 48]).is_ok()); + assert!(AsymmetricSecretKey::<V3>::from(&[0u8; 49]).is_err()); + + let mut pk2 = [0u8; 49]; + pk2[0] = 0x02; + let mut pk3 = [0u8; 49]; + pk3[0] = 0x03; + assert!(AsymmetricPublicKey::<V3>::from(&[0u8; 48]).is_err()); + assert!(AsymmetricPublicKey::<V3>::from(&[0u8; 49]).is_err()); + assert!(AsymmetricPublicKey::<V3>::from(&pk2).is_ok()); + assert!(AsymmetricPublicKey::<V3>::from(&pk3).is_ok()); + assert!(AsymmetricPublicKey::<V3>::from(&[0u8; 50]).is_err()); + } + + #[test] + fn try_from_secret_to_public() { + let kpv3 = AsymmetricKeyPair::<V3>::generate().unwrap(); + let pubv3 = AsymmetricPublicKey::<V3>::try_from(&kpv3.secret).unwrap(); + assert_eq!(pubv3.as_bytes(), kpv3.public.as_bytes()); + assert_eq!(pubv3, kpv3.public); + } + + #[test] + fn test_trait_impls() { + let debug = format!("{:?}", AsymmetricKeyPair::<V3>::generate().unwrap().secret); + assert_eq!(debug, "AsymmetricSecretKey {***OMITTED***}"); + + let randomv = AsymmetricKeyPair::<V3>::generate().unwrap(); + let randomv2 = AsymmetricKeyPair::<V3>::generate().unwrap(); + assert_ne!(randomv.secret, randomv2.secret); + } + + #[test] + fn test_invalid_pk() { + let uc_badlen = [0u8; 96]; + let mut uc_badtag = [0u8; 97]; + uc_badtag[0] = 0x02; + + assert!(UncompressedPublicKey::try_from(uc_badlen.as_ref()).is_err()); + assert!(UncompressedPublicKey::try_from(uc_badtag.as_ref()).is_err()); + + let mut kpv3 = AsymmetricKeyPair::<V3>::generate().unwrap(); + kpv3.public.bytes[0] = 0x04; + assert!(UncompressedPublicKey::try_from(&kpv3.public).is_err()); + } + + #[test] + fn test_clone() { + let kp = AsymmetricKeyPair::<V3>::generate().unwrap(); + assert_eq!(kp.secret, kp.secret.clone()); + assert_eq!(kp.public, kp.public.clone()); + } +} diff --git a/vendor/pasetors/src/version4.rs b/vendor/pasetors/src/version4.rs new file mode 100644 index 000000000..858b4b9bc --- /dev/null +++ b/vendor/pasetors/src/version4.rs @@ -0,0 +1,983 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "v4")))] + +use core::convert::TryFrom; +use core::marker::PhantomData; + +use crate::common::{encode_b64, validate_footer_untrusted_token}; +use crate::errors::Error; +use crate::keys::{ + AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate, SymmetricKey, +}; +use crate::pae; +use crate::token::{Local, Public, TrustedToken, UntrustedToken}; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use blake2b::SecretKey as AuthKey; +use ed25519_compact::{KeyPair, PublicKey, SecretKey, Seed, Signature}; +use orion::hazardous::mac::blake2b; +use orion::hazardous::mac::blake2b::Blake2b; +use orion::hazardous::stream::xchacha20; +use subtle::ConstantTimeEq; +use xchacha20::Nonce as EncNonce; +use xchacha20::SecretKey as EncKey; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// Version 4 of the PASETO spec. +pub struct V4; + +impl Version for V4 { + const LOCAL_KEY: usize = 32; + const SECRET_KEY: usize = 32 + Self::PUBLIC_KEY; // Seed || PK + const PUBLIC_KEY: usize = 32; + const PUBLIC_SIG: usize = 64; + const LOCAL_NONCE: usize = 32; + const LOCAL_TAG: usize = 32; + const PUBLIC_HEADER: &'static str = "v4.public."; + const LOCAL_HEADER: &'static str = "v4.local."; + #[cfg(feature = "paserk")] + const PASERK_ID: usize = 44; + + fn validate_local_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::LOCAL_KEY { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_secret_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::SECRET_KEY { + return Err(Error::Key); + } + + let seed = Seed::from_slice(&key_bytes[..32]).map_err(|_| Error::Key)?; + let kp = KeyPair::from_seed(seed); + + if !bool::from(kp.pk.as_slice().ct_eq(&key_bytes[32..])) { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_public_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::PUBLIC_KEY { + return Err(Error::Key); + } + + Ok(()) + } +} + +impl TryFrom<&AsymmetricSecretKey<V4>> for AsymmetricPublicKey<V4> { + type Error = Error; + + fn try_from(value: &AsymmetricSecretKey<V4>) -> Result<Self, Self::Error> { + AsymmetricPublicKey::<V4>::from(&value.as_bytes()[32..]) + } +} + +impl Generate<AsymmetricKeyPair<V4>, V4> for AsymmetricKeyPair<V4> { + fn generate() -> Result<AsymmetricKeyPair<V4>, Error> { + let key_pair = KeyPair::generate(); + + let secret = AsymmetricSecretKey::<V4>::from(key_pair.sk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + let public = AsymmetricPublicKey::<V4>::from(key_pair.pk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + + Ok(Self { public, secret }) + } +} + +impl Generate<SymmetricKey<V4>, V4> for SymmetricKey<V4> { + fn generate() -> Result<SymmetricKey<V4>, Error> { + let mut rng_bytes = vec![0u8; V4::LOCAL_KEY]; + V4::validate_local_key(&rng_bytes)?; + getrandom::getrandom(&mut rng_bytes)?; + + Ok(Self { + bytes: rng_bytes, + phantom: PhantomData, + }) + } +} + +/// PASETO v4 public tokens. +pub struct PublicToken; + +impl PublicToken { + /// The header and purpose for the public token: `v4.public.`. + pub const HEADER: &'static str = "v4.public."; + + /// Create a public token. + pub fn sign( + secret_key: &AsymmetricSecretKey<V4>, + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let sk = SecretKey::from_slice(secret_key.as_bytes()).map_err(|_| Error::Key)?; + + let f = footer.unwrap_or(&[]); + let i = implicit_assert.unwrap_or(&[]); + let m2 = pae::pae(&[Self::HEADER.as_bytes(), message, f, i])?; + let sig = sk.sign(m2, None); + + let mut m_sig: Vec<u8> = Vec::from(message); + m_sig.extend_from_slice(sig.as_ref()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(m_sig)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Verify a public token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn verify( + public_key: &AsymmetricPublicKey<V4>, + token: &UntrustedToken<Public, V4>, + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let i = implicit_assert.unwrap_or(&[]); + let sm = token.untrusted_message(); + let m = token.untrusted_payload(); + let s = sm[m.len()..m.len() + V4::PUBLIC_SIG].as_ref(); + + let m2 = pae::pae(&[Self::HEADER.as_bytes(), m, f, i])?; + let pk: PublicKey = PublicKey::from_slice(public_key.as_bytes()).map_err(|_| Error::Key)?; + + debug_assert!(s.len() == V4::PUBLIC_SIG); + // If the below fails, it is an invalid signature. + let sig = Signature::from_slice(s).map_err(|_| Error::TokenValidation)?; + + if pk.verify(m2, &sig).is_ok() { + TrustedToken::_new(Self::HEADER, m, f, i) + } else { + Err(Error::TokenValidation) + } + } +} + +/// PASETO v4 local tokens. +pub struct LocalToken; + +impl LocalToken { + /// The header and purpose for the local token: `v4.local.`. + pub const HEADER: &'static str = "v4.local."; + + /// Domain separator for key-splitting the encryption key (21 in length as bytes). + const DOMAIN_SEPARATOR_ENC: &'static str = "paseto-encryption-key"; + + /// Domain separator for key-splitting the authentication key (24 in length as bytes). + const DOMAIN_SEPARATOR_AUTH: &'static str = "paseto-auth-key-for-aead"; + + const M1_LEN: usize = V4::LOCAL_NONCE + Self::DOMAIN_SEPARATOR_ENC.as_bytes().len(); + const M2_LEN: usize = V4::LOCAL_NONCE + Self::DOMAIN_SEPARATOR_AUTH.as_bytes().len(); + + /// Split the user-provided secret key into keys used for encryption and authentication. + fn key_split(sk: &[u8], n: &[u8]) -> Result<(EncKey, EncNonce, AuthKey), Error> { + debug_assert_eq!(n.len(), V4::LOCAL_NONCE); + debug_assert_eq!(sk.len(), V4::LOCAL_KEY); + + let mut m1 = [0u8; Self::M1_LEN]; + m1[..21].copy_from_slice(Self::DOMAIN_SEPARATOR_ENC.as_bytes()); + m1[21..].copy_from_slice(n); + + let mut m2 = [0u8; Self::M2_LEN]; + m2[..24].copy_from_slice(Self::DOMAIN_SEPARATOR_AUTH.as_bytes()); + m2[24..].copy_from_slice(n); + + let sk = blake2b::SecretKey::from_slice(sk).unwrap(); + let mut b2_ctx = Blake2b::new(&sk, 56).unwrap(); + b2_ctx.update(&m1).unwrap(); + let tmp = b2_ctx.finalize().unwrap(); + let enc_key = EncKey::from_slice(&tmp.unprotected_as_bytes()[..32]).unwrap(); + let n2 = EncNonce::from_slice(&tmp.unprotected_as_bytes()[32..]).unwrap(); + + b2_ctx = Blake2b::new(&sk, V4::LOCAL_TAG).unwrap(); + b2_ctx.update(&m2).unwrap(); + let auth_key = + AuthKey::from_slice(b2_ctx.finalize().unwrap().unprotected_as_bytes()).unwrap(); + + Ok((enc_key, n2, auth_key)) + } + + /// Encrypt and authenticate a message using nonce directly. + pub(crate) fn encrypt_with_nonce( + secret_key: &SymmetricKey<V4>, + nonce: &[u8], + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + debug_assert_eq!(nonce.len(), V4::LOCAL_NONCE); + let f = footer.unwrap_or(&[]); + let i = implicit_assert.unwrap_or(&[]); + + let (enc_key, n2, auth_key) = Self::key_split(secret_key.as_bytes(), nonce)?; + + let mut ciphertext = vec![0u8; message.len()]; + xchacha20::encrypt(&enc_key, &n2, 0, message, &mut ciphertext) + .map_err(|_| Error::Encryption)?; + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), nonce, ciphertext.as_slice(), f, i])?; + + let mut b2_ctx = Blake2b::new(&auth_key, V4::LOCAL_TAG).unwrap(); + b2_ctx + .update(pre_auth.as_slice()) + .map_err(|_| Error::Encryption)?; + let tag = b2_ctx.finalize().map_err(|_| Error::Encryption)?; + + // nonce and tag lengths are both 32, so obviously safe to op::add + let concat_len: usize = match (nonce.len() + tag.len()).checked_add(ciphertext.len()) { + Some(len) => len, + None => return Err(Error::Encryption), + }; + let mut concat = vec![0u8; concat_len]; + concat[..32].copy_from_slice(nonce); + concat[32..32 + ciphertext.len()].copy_from_slice(ciphertext.as_slice()); + concat[concat_len - V4::LOCAL_TAG..].copy_from_slice(tag.unprotected_as_bytes()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(concat)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Create a local token. + pub fn encrypt( + secret_key: &SymmetricKey<V4>, + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let mut n = [0u8; V4::LOCAL_NONCE]; + getrandom::getrandom(&mut n)?; + + Self::encrypt_with_nonce(secret_key, &n, message, footer, implicit_assert) + } + + #[allow(clippy::many_single_char_names)] // The single-char names match those in the spec + /// Verify and decrypt a local token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn decrypt( + secret_key: &SymmetricKey<V4>, + token: &UntrustedToken<Local, V4>, + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let i = implicit_assert.unwrap_or(&[]); + let nc = token.untrusted_message(); + + let mut n: [u8; 32] = [0u8; V4::LOCAL_NONCE]; + n.copy_from_slice(nc[..V4::LOCAL_NONCE].as_ref()); + let c = token.untrusted_payload(); + let t = nc[nc.len() - V4::LOCAL_TAG..].as_ref(); + + let (enc_key, n2, auth_key) = Self::key_split(secret_key.as_bytes(), &n)?; + + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), n.as_ref(), c, f, i])?; + let expected_tag = blake2b::Tag::from_slice(t).map_err(|_| Error::TokenValidation)?; + blake2b::Blake2b::verify(&expected_tag, &auth_key, 32, pre_auth.as_slice()) + .map_err(|_| Error::TokenValidation)?; + + let mut out = vec![0u8; c.len()]; + xchacha20::decrypt(&enc_key, &n2, 0, c, &mut out).map_err(|_| Error::TokenValidation)?; + + TrustedToken::_new(Self::HEADER, &out, f, i) + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod test_vectors { + + use hex; + + use super::*; + use core::convert::TryFrom; + use std::fs::File; + use std::io::BufReader; + + use crate::claims::Claims; + use crate::common::tests::*; + + fn test_local(test: &PasetoTest) { + debug_assert!(test.nonce.is_some()); + debug_assert!(test.key.is_some()); + + let sk = + SymmetricKey::<V4>::from(&hex::decode(test.key.as_ref().unwrap()).unwrap()).unwrap(); + + let nonce = hex::decode(test.nonce.as_ref().unwrap()).unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + let implicit_assert = test.implicit_assertion.as_bytes(); + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Local, V4>::try_from(&test.token) { + assert!(LocalToken::decrypt(&sk, &ut, footer, Some(implicit_assert)).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = LocalToken::encrypt_with_nonce( + &sk, + &nonce, + message.as_bytes(), + footer, + Some(implicit_assert), + ) + .unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + + let ut = UntrustedToken::<Local, V4>::try_from(&test.token).unwrap(); + let trusted = LocalToken::decrypt(&sk, &ut, footer, Some(implicit_assert)).unwrap(); + assert_eq!(trusted.payload(), message, "Failed {:?}", test.name); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), LocalToken::HEADER); + assert_eq!(trusted.implicit_assert(), implicit_assert); + + let parsed_claims = Claims::from_bytes(trusted.payload().as_bytes()).unwrap(); + let test_vector_claims = serde_json::from_str::<Payload>(message).unwrap(); + + assert_eq!( + parsed_claims.get_claim("data").unwrap().as_str().unwrap(), + test_vector_claims.data, + ); + assert_eq!( + parsed_claims.get_claim("exp").unwrap().as_str().unwrap(), + test_vector_claims.exp, + ); + } + + fn test_public(test: &PasetoTest) { + debug_assert!(test.public_key.is_some()); + debug_assert!(test.secret_key.is_some()); + + let sk = AsymmetricSecretKey::<V4>::from( + &hex::decode(test.secret_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let pk = AsymmetricPublicKey::<V4>::from( + &hex::decode(test.public_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + let implicit_assert = test.implicit_assertion.as_bytes(); + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Public, V4>::try_from(&test.token) { + assert!(PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = + PublicToken::sign(&sk, message.as_bytes(), footer, Some(implicit_assert)).unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + let ut = UntrustedToken::<Public, V4>::try_from(&test.token).unwrap(); + + let trusted = PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).unwrap(); + assert_eq!(trusted.payload(), message); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), PublicToken::HEADER); + assert_eq!(trusted.implicit_assert(), implicit_assert); + } + + #[test] + fn run_test_vectors() { + let path = "./test_vectors/v4.json"; + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for t in tests.tests { + // v4.public + if t.public_key.is_some() { + test_public(&t); + } + // v4.local + if t.nonce.is_some() { + test_local(&t); + } + } + } +} + +#[cfg(test)] +mod test_tokens { + use super::*; + use crate::common::decode_b64; + use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; + use crate::token::UntrustedToken; + use core::convert::TryFrom; + + const TEST_LOCAL_SK_BYTES: [u8; 32] = [ + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, + 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + ]; + + pub(crate) const TEST_SK_BYTES: [u8; 64] = [ + 180, 203, 251, 67, 223, 76, 226, 16, 114, 125, 149, 62, 74, 113, 51, 7, 250, 25, 187, 125, + 159, 133, 4, 20, 56, 217, 225, 27, 148, 42, 55, 116, 30, 185, 219, 187, 188, 4, 124, 3, + 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, 117, 114, 37, 193, 31, 0, 65, 93, + 14, 32, 177, 162, + ]; + + const TEST_PK_BYTES: [u8; 32] = [ + 30, 185, 219, 187, 188, 4, 124, 3, 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, + 117, 114, 37, 193, 31, 0, 65, 93, 14, 32, 177, 162, + ]; + + const MESSAGE: &str = + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"; + const FOOTER: &str = "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"; + const VALID_PUBLIC_TOKEN: &str = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const VALID_LOCAL_TOKEN: &str = "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + #[test] + fn test_gen_keypair() { + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + + let token = PublicToken::sign(&kp.secret, MESSAGE.as_bytes(), None, None).unwrap(); + + let ut = UntrustedToken::<Public, V4>::try_from(&token).unwrap(); + assert!(PublicToken::verify(&kp.public, &ut, None, None).is_ok()); + } + + #[test] + fn test_untrusted_token_usage() { + // Local + let sk = SymmetricKey::<V4>::generate().unwrap(); + let token = + LocalToken::encrypt(&sk, MESSAGE.as_bytes(), Some(FOOTER.as_bytes()), None).unwrap(); + + let untrusted_token = UntrustedToken::<Local, V4>::try_from(token.as_str()).unwrap(); + let _ = LocalToken::decrypt( + &sk, + &untrusted_token, + Some(untrusted_token.untrusted_footer()), + None, + ) + .unwrap(); + + // Public + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + let token = PublicToken::sign( + &kp.secret, + MESSAGE.as_bytes(), + Some(FOOTER.as_bytes()), + None, + ) + .unwrap(); + + let untrusted_token = UntrustedToken::<Public, V4>::try_from(token.as_str()).unwrap(); + assert!( + PublicToken::verify(&kp.public, &untrusted_token, Some(FOOTER.as_bytes()), None) + .is_ok() + ); + } + + #[test] + fn test_roundtrip_local() { + let sk = SymmetricKey::<V4>::generate().unwrap(); + let message = "token payload"; + + let token = LocalToken::encrypt(&sk, message.as_bytes(), None, None).unwrap(); + let ut = UntrustedToken::<Local, V4>::try_from(&token).unwrap(); + let trusted_token = LocalToken::decrypt(&sk, &ut, None, None).unwrap(); + + assert_eq!(trusted_token.payload(), message); + } + + #[test] + fn test_roundtrip_public() { + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + + let token = PublicToken::sign(&test_sk, MESSAGE.as_bytes(), None, None).unwrap(); + let ut = UntrustedToken::<Public, V4>::try_from(&token).unwrap(); + + assert!(PublicToken::verify(&test_pk, &ut, None, None).is_ok()); + } + + #[test] + fn footer_logic() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + + // We create a token with Some(footer) and with None + let actual_some = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, Some(FOOTER.as_bytes()), None).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + + // token = Some(footer) = validate and compare + // token = None(footer) = validate only + + // We should be able to validate with None if created with Some() (excludes constant-time + // comparison with known value) + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + // We should be able to validate with Some() if created with Some() + assert!(PublicToken::verify(&test_pk, &actual_some, Some(FOOTER.as_bytes()), None).is_ok()); + // We should NOT be able to validate with Some() if created with None + assert!( + PublicToken::verify(&test_pk, &actual_none, Some(FOOTER.as_bytes()), None).is_err() + ); + + let actual_some = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, Some(FOOTER.as_bytes()), None).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, None).unwrap(), + ) + .unwrap(); + + // They don't equal because the nonce is random. So we only check decryption. + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, None, None).is_ok()); + assert!( + LocalToken::decrypt(&test_local_sk, &actual_some, Some(FOOTER.as_bytes()), None) + .is_ok() + ); + assert!( + LocalToken::decrypt(&test_local_sk, &actual_none, Some(FOOTER.as_bytes()), None) + .is_err() + ); + } + + #[test] + fn implicit_none_some_empty_is_same() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + let implicit = b""; + + let actual_some = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, Some(implicit)).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + assert_eq!(actual_some, actual_none); + + assert!(PublicToken::verify(&test_pk, &actual_none, None, Some(implicit)).is_ok()); + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + + let actual_some = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, Some(implicit)).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, None).unwrap(), + ) + .unwrap(); + // They don't equal because the nonce is random. So we only check decryption. + + assert!(LocalToken::decrypt(&test_local_sk, &actual_none, None, Some(implicit)).is_ok()); + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, None, None).is_ok()); + } + + #[test] + // NOTE: See https://github.com/paseto-standard/paseto-spec/issues/17 + fn empty_payload() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::sign(&test_sk, b"", None, None).unwrap_err(), + Error::EmptyPayload + ); + assert_eq!( + LocalToken::encrypt(&test_local_sk, b"", None, None).unwrap_err(), + Error::EmptyPayload + ); + } + + #[test] + fn err_on_modified_footer() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_wrong_implicit_assert() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + assert!(PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .is_ok()); + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + Some(b"WRONG IMPLICIT") + ) + .unwrap_err(), + Error::TokenValidation + ); + assert!(LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .is_ok()); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + Some(b"WRONG IMPLICIT") + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_footer_in_token_none_supplied() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(b""), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(b""), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_no_footer_in_token_some_supplied() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_public: String = format!( + "{}.{}.{}", + split_public[0], split_public[1], split_public[2] + ); + + let split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_local: String = + format!("{}.{}.{}", split_local[0], split_local[1], split_local[2]); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_signature() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + + let mut split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_sig = decode_b64(split_public[2]).unwrap(); + bad_sig.copy_within(0..32, 32); + let tmp = encode_b64(bad_sig).unwrap(); + split_public[2] = &tmp; + let invalid_public: String = format!( + "{}.{}.{}.{}", + split_public[0], split_public[1], split_public[2], split_public[3] + ); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_tag() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_tag = decode_b64(split_local[2]).unwrap(); + let tlen = bad_tag.len(); + bad_tag.copy_within(0..16, tlen - 16); + let tmp = encode_b64(bad_tag).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_ciphertext() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_ct = decode_b64(split_local[2]).unwrap(); + let ctlen = bad_ct.len(); + bad_ct.copy_within((ctlen - 16)..ctlen, 24); + let tmp = encode_b64(bad_ct).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_nonce() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_nonce = decode_b64(split_local[2]).unwrap(); + let nlen = bad_nonce.len(); + bad_nonce.copy_within((nlen - 24)..nlen, 0); + let tmp = encode_b64(bad_nonce).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_public_secret_key() { + let bad_pk = AsymmetricPublicKey::<V4>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + PublicToken::verify( + &bad_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_shared_secret_key() { + let bad_local_sk = SymmetricKey::<V4>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + LocalToken::decrypt( + &bad_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } +} + +#[cfg(test)] +mod test_keys { + use super::*; + use crate::version4::test_tokens::TEST_SK_BYTES; + + #[test] + fn test_symmetric_gen() { + let randomv = SymmetricKey::<V4>::generate().unwrap(); + assert_ne!(randomv.as_bytes(), &[0u8; 32]); + } + + #[test] + fn test_invalid_sizes() { + assert!(AsymmetricSecretKey::<V4>::from(&[1u8; 63]).is_err()); + assert!(AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).is_ok()); + assert!(AsymmetricSecretKey::<V4>::from(&[1u8; 65]).is_err()); + + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 31]).is_err()); + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 32]).is_ok()); + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 33]).is_err()); + + assert!(SymmetricKey::<V4>::from(&[0u8; 31]).is_err()); + assert!(SymmetricKey::<V4>::from(&[0u8; 32]).is_ok()); + assert!(SymmetricKey::<V4>::from(&[0u8; 33]).is_err()); + } + + #[test] + fn try_from_secret_to_public() { + let kpv4 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let pubv4 = AsymmetricPublicKey::<V4>::try_from(&kpv4.secret).unwrap(); + assert_eq!(pubv4.as_bytes(), kpv4.public.as_bytes()); + assert_eq!(pubv4, kpv4.public); + assert_eq!(&kpv4.secret.as_bytes()[32..], pubv4.as_bytes()); + } + + #[test] + fn test_trait_impls() { + let debug = format!("{:?}", SymmetricKey::<V4>::generate().unwrap()); + assert_eq!(debug, "SymmetricKey {***OMITTED***}"); + + let randomv = SymmetricKey::<V4>::generate().unwrap(); + let zero = SymmetricKey::<V4>::from(&[0u8; V4::LOCAL_KEY]).unwrap(); + assert_ne!(randomv, zero); + + let debug = format!("{:?}", AsymmetricKeyPair::<V4>::generate().unwrap().secret); + assert_eq!(debug, "AsymmetricSecretKey {***OMITTED***}"); + + let random1 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let random2 = AsymmetricKeyPair::<V4>::generate().unwrap(); + assert_ne!(random1.secret, random2.secret); + } + + #[test] + fn test_clone() { + let sk = SymmetricKey::<V4>::generate().unwrap(); + assert_eq!(sk, sk.clone()); + + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + assert_eq!(kp.secret, kp.secret.clone()); + assert_eq!(kp.public, kp.public.clone()); + } +} |