summaryrefslogtreecommitdiffstats
path: root/vendor/pasetors/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/pasetors/src
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-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.rs734
-rw-r--r--vendor/pasetors/src/common.rs97
-rw-r--r--vendor/pasetors/src/errors.rs76
-rw-r--r--vendor/pasetors/src/footer.rs396
-rw-r--r--vendor/pasetors/src/keys.rs144
-rw-r--r--vendor/pasetors/src/lib.rs360
-rw-r--r--vendor/pasetors/src/pae.rs78
-rw-r--r--vendor/pasetors/src/paserk.rs860
-rw-r--r--vendor/pasetors/src/serde.rs133
-rw-r--r--vendor/pasetors/src/token.rs656
-rw-r--r--vendor/pasetors/src/version.rs37
-rw-r--r--vendor/pasetors/src/version2.rs828
-rw-r--r--vendor/pasetors/src/version3.rs817
-rw-r--r--vendor/pasetors/src/version4.rs983
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());
+ }
+}