diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ec55755..c02d4a8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,8 +16,11 @@ permissions: contents: read jobs: default-features: - name: Default Features (batteries_included + v4) + name: Default Features (batteries_included + v4 + (${{ matrix.backend }})) runs-on: ubuntu-latest + strategy: + matrix: + backend: [time, chrono] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 @@ -28,13 +31,10 @@ jobs: components: clippy - name: Install nextest uses: taiki-e/install-action@nextest - - name: Clippy (default features) - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings - - name: Test (default features) - run: cargo nextest run + - name: Clippy (default features + ${{ matrix.backend }}) + run: cargo clippy --features ${{ matrix.backend }} -- -D warnings + - name: Test (default features + ${{ matrix.backend }}) + run: cargo nextest run --features ${{ matrix.backend }} test: name: Test Suite runs-on: ubuntu-latest @@ -58,8 +58,10 @@ jobs: override: true - name: Install nextest uses: taiki-e/install-action@nextest - - name: Run tests - run: cargo nextest run --no-default-features --features ${{ matrix.feature }} + - name: Run tests (time) + run: cargo nextest run --no-default-features --features ${{ matrix.feature }},time + - name: Run tests (chrono) + run: cargo nextest run --no-default-features --features ${{ matrix.feature }},chrono clippy: name: Clippy runs-on: ubuntu-latest @@ -82,10 +84,16 @@ jobs: toolchain: stable override: true components: clippy - - uses: actions-rs/cargo@v1 + - name: Clippy (time) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-default-features --features ${{ matrix.feature }},time -- -D warnings + - name: Clippy (chrono) + uses: actions-rs/cargo@v1 with: command: clippy - args: --no-default-features --features ${{ matrix.feature }} -- -D warnings + args: --no-default-features --features ${{ matrix.feature }},chrono -- -D warnings audit: name: Security Audit runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index ccdef24..ed27a61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,9 @@ v4_public = ["v4", "public", "core", "ed25519-dalek", "ring/std"] core = [] generic = ["core", "serde", "erased-serde", "serde_json"] batteries_included = ["generic"] +# Time backend features (mutually exclusive) +time = ["dep:time"] +chrono = ["dep:chrono"] default = ["batteries_included", "v4_local", "v4_public"] paserk = ["dep:paserk"] @@ -78,7 +81,8 @@ hmac = { version = "0.12", optional = true } sha2 = { version = "0.10", optional = true } subtle = "2.6" zeroize = { version = "1.8", features = ["zeroize_derive"] } -time = { version = "0.3.47", features = ["parsing", "formatting"] } +time = { version = "0.3.47", features = ["parsing", "formatting"], optional = true } +chrono = { version = "0.4", features = ["std"], optional = true } rand_core = "0.9" digest = "0.10" paserk = { version = "0.4.0", optional = true } diff --git a/src/error.rs b/src/error.rs index 4358ce0..6b24a96 100644 --- a/src/error.rs +++ b/src/error.rs @@ -136,8 +136,14 @@ pub enum Error { // ==================== Time Format Errors ==================== /// Error formatting time values + #[cfg(feature = "time")] #[error("time format error: {0}")] TimeFormat(#[from] time::error::Format), + + /// Error parsing time values + #[cfg(feature = "chrono")] + #[error("chrono parse error: {0}")] + ChronoParse(#[from] chrono::ParseError), } /// A specialized Result type for rusty_paseto operations. diff --git a/src/generic/claims/mod.rs b/src/generic/claims/mod.rs index 7f7e00c..e0d7b30 100644 --- a/src/generic/claims/mod.rs +++ b/src/generic/claims/mod.rs @@ -32,15 +32,26 @@ mod unit_tests { //TODO: need more comprehensive tests than these to flesh out the additionl error types use super::*; use anyhow::Result; - //use chrono::prelude::*; use std::convert::TryFrom; + + #[cfg(feature = "time")] use time::format_description::well_known::Rfc3339; + #[cfg(feature = "time")] + fn now_rfc3339() -> String { + time::OffsetDateTime::now_utc().format(&Rfc3339).expect("format failed") + } + + #[cfg(feature = "chrono")] + fn now_rfc3339() -> String { + chrono::Utc::now().to_rfc3339() + } + #[test] fn test_expiration_claim() -> Result<()> { // setup // a good time format - let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; + let now = now_rfc3339(); assert!(ExpirationClaim::try_from("hello").is_err()); let claim = ExpirationClaim::try_from(now); @@ -56,7 +67,7 @@ mod unit_tests { fn test_not_before_claim() -> Result<()> { // setup // a good time format - let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; + let now = now_rfc3339(); assert!(NotBeforeClaim::try_from("hello").is_err()); let claim = NotBeforeClaim::try_from(now); @@ -72,7 +83,7 @@ mod unit_tests { fn test_issued_at_claim() -> Result<()> { // setup // a good time format - let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; + let now = now_rfc3339(); assert!(IssuedAtClaim::try_from("hello").is_err()); let claim = IssuedAtClaim::try_from(now); diff --git a/src/lib.rs b/src/lib.rs index 8532d8e..286501c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ //! //! //! # Usage -//! `rusty_paseto` is meant to be flexible and configurable for your specific use case. Whether you want to get started quickly with sensible defaults, create your own version of `rusty_paseto` in order to customize your own defaults and functionality or just want to use the core PASETO crypto features, the crate is heavily feature gated to allow for your needs. +//! `rusty_paseto` is meant to be flexible and configurable for your specific use case. Whether you want to get started quickly with sensible defaults, create your own version of `rusty_paseto` in order to customize your own defaults and functionality or just want to use the core PASETO crypto features, the crate is heavily feature gated to allow for your needs. //! ## Architecture @@ -164,7 +164,7 @@ //! ``` //! ### core //! -//! The core architectural layer is the most basic PASETO implementation as it accepts a Payload, optional Footer and (if v3 or v4) an optional Implicit Assertion along with the appropriate key to encrypt/sign and decrypt/verify basic strings. +//! The core architectural layer is the most basic PASETO implementation as it accepts a Payload, optional Footer and (if v3 or v4) an optional Implicit Assertion along with the appropriate key to encrypt/sign and decrypt/verify basic strings. //! //! //! @@ -386,9 +386,13 @@ //! // must include //! use std::convert::TryFrom; //! let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); -//! // real-world example using the time crate to expire 5 minutes from now +//! // real-world example: expire 5 minutes from now +//! # #[cfg(feature = "time")] //! # use time::format_description::well_known::Rfc3339; +//! # #[cfg(feature = "time")] //! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; +//! # #[cfg(feature = "chrono")] +//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339(); //! //! let token = PasetoBuilder::::default() //! // note the TryFrom implmentation for ExpirationClaim @@ -406,7 +410,7 @@ //! //! A **1 hour** `ExpirationClaim` is set by default because the use case for non-expiring tokens in the world of security tokens is fairly limited. //! Omitting an expiration claim or forgetting to require one when processing them -//! is almost certainly an oversight rather than a deliberate choice. +//! is almost certainly an oversight rather than a deliberate choice. //! //! When it is a deliberate choice, you have the opportunity to deliberately remove this claim from the Builder. //! The method call required to do so ensures readers of the code understand the implicit risk. @@ -414,10 +418,14 @@ //! # #[cfg(feature = "default")] //! # { //! # use rusty_paseto::prelude::*; -//! # use time::format_description::well_known::Rfc3339; //! # use std::convert::TryFrom; -//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); +//! # #[cfg(feature = "time")] +//! # use time::format_description::well_known::Rfc3339; +//! # #[cfg(feature = "time")] +//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; +//! # #[cfg(feature = "chrono")] +//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339(); //! let token = PasetoBuilder::::default() //! .set_claim(ExpirationClaim::try_from(in_5_minutes)?) //! // even if you set an expiration claim (as above) it will be ignored @@ -435,15 +443,20 @@ //! # #[cfg(all(test,feature = "v4_local"))] //! # { //! # use rusty_paseto::prelude::*; -//! # use time::format_description::well_known::Rfc3339; //! # // must include //! # use std::convert::TryFrom; //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); -//! # // real-world example using the time crate to expire 5 minutes from now +//! # #[cfg(feature = "time")] +//! # use time::format_description::well_known::Rfc3339; +//! # #[cfg(feature = "time")] //! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; -//! // real-world example using the time crate to prevent the token from being used before 2 -//! // minutes from now +//! # #[cfg(feature = "chrono")] +//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339(); +//! // set not-before to 2 minutes in the future +//! # #[cfg(feature = "time")] //! let in_2_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(2)).format(&Rfc3339)?; +//! # #[cfg(feature = "chrono")] +//! # let in_2_minutes = (chrono::Utc::now() + chrono::Duration::minutes(2)).to_rfc3339(); //! //! let token = PasetoBuilder::::default() //! //json payload key: "exp" @@ -671,6 +684,13 @@ compile_error!( See: https://github.com/rrrodzilla/rusty_paseto/issues/48" ); +// Ensure exactly one time backend is selected when the prelude is used. +#[cfg(all(feature = "time", feature = "chrono"))] +compile_error!("features `time` and `chrono` are mutually exclusive. Enable exactly one time backend"); + +#[cfg(all(not(feature = "time"), not(feature = "chrono")))] +compile_error!("a time backend is required. Enable either the `time` feature or `chrono`"); + //public interface #[cfg(feature = "core")] pub mod core; diff --git a/src/prelude/error.rs b/src/prelude/error.rs index 3d41c3a..c6db6cc 100644 --- a/src/prelude/error.rs +++ b/src/prelude/error.rs @@ -21,5 +21,10 @@ pub enum GeneralPasetoError { }, /// An error with the data format #[error(transparent)] + #[cfg(feature = "time")] RFC3339Date(#[from] time::error::Format), + /// An error parsing a date/time value with chrono + #[error(transparent)] + #[cfg(feature = "chrono")] + ChronoParse(#[from] chrono::ParseError), } diff --git a/src/prelude/paseto_builder.rs b/src/prelude/paseto_builder.rs index 5beae30..ca0874e 100644 --- a/src/prelude/paseto_builder.rs +++ b/src/prelude/paseto_builder.rs @@ -2,8 +2,10 @@ use crate::generic::*; use core::marker::PhantomData; use std::collections::HashSet; use std::convert::TryFrom; + +#[cfg(feature = "time")] use time::format_description::well_known::Rfc3339; - ///The `PasetoBuilder` is created at compile time by specifying a PASETO version and purpose and +///The `PasetoBuilder` is created at compile time by specifying a PASETO version and purpose and ///providing a key of the same version and purpose. This structure allows setting [PASETO claims](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md), ///your own [custom claims](CustomClaim), an optional [footer](Footer) and in the case of V3/V4 tokens, an optional [implicit ///assertion](ImplicitAssertion). @@ -19,9 +21,9 @@ use time::format_description::well_known::Rfc3339; ///# #[cfg(all(feature = "prelude", feature="v2_local"))] ///# { /// use rusty_paseto::prelude::*; - /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); - /// let footer = Footer::from("some footer"); - /// //create a builder, add some claims and then build the token with the key +/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); +/// let footer = Footer::from("some footer"); +/// //create a builder, add some claims and then build the token with the key /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) /// .set_claim(SubjectClaim::from("loyal subjects")) @@ -35,11 +37,11 @@ use time::format_description::well_known::Rfc3339; /// .set_claim(CustomClaim::try_from(("pi to 6 digits", 3.141526))?) /// .set_footer(footer) /// .try_encrypt(&key)?; - /// //now let's decrypt the token and verify the values +/// //now let's decrypt the token and verify the values /// let json = PasetoParser::::default() /// .set_footer(footer) /// .parse(&token, &key)?; - /// assert_eq!(json["aud"], "customers"); +/// assert_eq!(json["aud"], "customers"); /// assert_eq!(json["jti"], "me"); /// assert_eq!(json["iss"], "me"); /// assert_eq!(json["data"], "this is a secret message"); @@ -72,7 +74,7 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { dup_top_level_found: (false, String::default()), } } - /// Given a [`PasetoClaim`], attempts to add it to the builder for inclusion in the payload of the + /// Given a [`PasetoClaim`], attempts to add it to the builder for inclusion in the payload of the /// token. /// claims provided to the `GenericBuilder`. Overwrites the default 'nbf' (not before) claim if /// provided. Prevents duplicate claims from being added. @@ -113,10 +115,10 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { self.builder.set_claim(value); self } - /// Sets the token to have no expiration date. + /// Sets the token to have no expiration date. /// A **1 hour** `ExpirationClaim` is set by default because the use case for non-expiring tokens in the world of security tokens is fairly limited. /// Omitting an expiration claim or forgetting to require one when processing them - /// is almost certainly an oversight rather than a deliberate choice. + /// is almost certainly an oversight rather than a deliberate choice. /// When it is a deliberate choice, you have the opportunity to deliberately remove this claim from the Builder. /// This method call ensures readers of the code understand the implicit risk. /// @@ -145,7 +147,7 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { self.non_expiring_token = true; self } - /// Sets an optional [Footer] on the token. + /// Sets an optional [Footer] on the token. /// /// Returns a mutable reference to the builder on success. /// @@ -304,18 +306,27 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { /// # Ok::<(),anyhow::Error>(()) /// ``` pub fn expires_in(mut self, duration: std::time::Duration) -> Self { - let now = time::OffsetDateTime::now_utc(); - // Convert std::time::Duration to time::Duration - let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::hours(1)); - - if let Some(expiration) = now.checked_add(time_duration) { - if let Ok(formatted) = expiration.format(&Rfc3339) { - if let Ok(exp_claim) = ExpirationClaim::try_from(formatted) { - // Remove existing expiration claim and add new one - self.builder.remove_claim("exp"); - self.top_level_claims.remove("exp"); - self.set_claim(exp_claim); - } + #[cfg(feature = "time")] + let formatted: Option = { + let now = time::OffsetDateTime::now_utc(); + // Convert std::time::Duration to time::Duration + let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::hours(1)); + now.checked_add(time_duration).and_then(|t| t.format(&Rfc3339).ok()) + }; + #[cfg(feature = "chrono")] + let formatted: Option = { + let now = chrono::Utc::now(); + // Convert std::time::Duration to chrono::Duration + let chrono_duration = chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::hours(1)); + Some((now + chrono_duration).to_rfc3339()) + }; + + if let Some(f) = formatted { + if let Ok(expiration) = ExpirationClaim::try_from(f) { + // Remove existing expiration claim and add new one + self.builder.remove_claim("exp"); + self.top_level_claims.remove("exp"); + self.set_claim(expiration); } } self @@ -340,17 +351,26 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { /// # Ok::<(),anyhow::Error>(()) /// ``` pub fn not_before_in(mut self, duration: std::time::Duration) -> Self { - let now = time::OffsetDateTime::now_utc(); - // Convert std::time::Duration to time::Duration - let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO); - - if let Some(not_before) = now.checked_add(time_duration) { - if let Ok(formatted) = not_before.format(&Rfc3339) { - if let Ok(nbf_claim) = NotBeforeClaim::try_from(formatted) { - // Remove existing nbf claim and add new one - self.builder.remove_claim("nbf"); - self.set_claim(nbf_claim); - } + #[cfg(feature = "time")] + let formatted: Option = { + let now = time::OffsetDateTime::now_utc(); + // Convert std::time::Duration to time::Duration + let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO); + now.checked_add(time_duration).and_then(|t| t.format(&Rfc3339).ok()) + }; + #[cfg(feature = "chrono")] + let formatted: Option = { + let now = chrono::Utc::now(); + // Convert std::time::Duration to chrono::Duration + let chrono_duration = chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::zero()); + Some((now + chrono_duration).to_rfc3339()) + }; + + if let Some(f) = formatted { + if let Ok(nbf_claim) = NotBeforeClaim::try_from(f) { + // Remove existing nbf claim and add new one + self.builder.remove_claim("nbf"); + self.set_claim(nbf_claim); } } self @@ -400,19 +420,39 @@ where impl<'a, Version, Purpose> Default for PasetoBuilder<'a, Version, Purpose> { fn default() -> Self { let mut new_builder = Self::new(); - let now = time::OffsetDateTime::now_utc(); - - // Use checked_add to handle potential overflow (though unlikely with 1 hour duration) - // RFC3339 formatting of valid OffsetDateTime values should be infallible - // but we handle potential failures gracefully by skipping claim creation - if let Some(in_one_hour) = now.checked_add(time::Duration::hours(1)) { - if let Ok(expiration_time) = in_one_hour.format(&Rfc3339) { - if let Ok(exp_claim) = ExpirationClaim::try_from(expiration_time) { - new_builder.builder.set_claim(exp_claim); + + #[cfg(feature = "time")] + { + let now = time::OffsetDateTime::now_utc(); + + // Use checked_add to handle potential overflow (though unlikely with 1 hour duration) + // RFC3339 formatting of valid OffsetDateTime values should be infallible + // but we handle potential failures gracefully by skipping claim creation + if let Some(in_one_hour) = now.checked_add(time::Duration::hours(1)) { + if let Ok(expiration_time) = in_one_hour.format(&Rfc3339) { + if let Ok(exp_claim) = ExpirationClaim::try_from(expiration_time) { + new_builder.builder.set_claim(exp_claim); + } + } + } + if let Ok(current_time) = now.format(&Rfc3339) { + if let Ok(iat_claim) = IssuedAtClaim::try_from(current_time.clone()) { + new_builder.builder.set_claim(iat_claim); + } + if let Ok(nbf_claim) = NotBeforeClaim::try_from(current_time) { + new_builder.builder.set_claim(nbf_claim); } } } - if let Ok(current_time) = now.format(&Rfc3339) { + + #[cfg(feature = "chrono")] + { + let now = chrono::Utc::now(); + let in_one_hour = now + chrono::Duration::hours(1); + if let Ok(exp_claim) = ExpirationClaim::try_from(in_one_hour.to_rfc3339()) { + new_builder.builder.set_claim(exp_claim); + } + let current_time = now.to_rfc3339(); if let Ok(iat_claim) = IssuedAtClaim::try_from(current_time.clone()) { new_builder.builder.set_claim(iat_claim); } @@ -440,7 +480,7 @@ impl PasetoBuilder<'_, V1, Local> { ///# { /// use rusty_paseto::prelude::*; /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); - /// let footer = Footer::from("some footer"); + /// let footer = Footer::from("some footer"); /// //create a builder, add some claims and then build the token with the key /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) @@ -495,7 +535,7 @@ impl PasetoBuilder<'_, V2, Local> { ///# { /// use rusty_paseto::prelude::*; /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); - /// let footer = Footer::from("some footer"); + /// let footer = Footer::from("some footer"); /// //create a builder, add some claims and then build the token with the key /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) @@ -549,7 +589,7 @@ impl PasetoBuilder<'_, V3, Local> { ///# { /// use rusty_paseto::prelude::*; /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); - /// let footer = Footer::from("some footer"); + /// let footer = Footer::from("some footer"); /// let implicit_assertion = ImplicitAssertion::from("some assertion"); /// //create a builder, add some claims and then build the token with the key /// let token = PasetoBuilder::::default() @@ -606,7 +646,7 @@ impl PasetoBuilder<'_, V4, Local> { ///# { /// use rusty_paseto::prelude::*; /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub")); - /// let footer = Footer::from("some footer"); + /// let footer = Footer::from("some footer"); /// let implicit_assertion = ImplicitAssertion::from("some assertion"); /// //create a builder, add some claims and then build the token with the key /// let token = PasetoBuilder::::default() @@ -672,7 +712,7 @@ impl PasetoBuilder<'_, V1, Public> { /// #[allow(deprecated)] /// let private_key = PasetoAsymmetricPrivateKey::::from(pk); /// let footer = Footer::from("some footer"); - /// //sign a public V1 token + /// //sign a public V1 token /// #[allow(deprecated)] /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) @@ -722,8 +762,8 @@ impl PasetoBuilder<'_, V1, Public> { /// # Ok::<(),anyhow::Error>(()) ///``` #[deprecated( - since = "0.8.1", - note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." + since = "0.8.1", + note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." )] #[allow(deprecated)] pub fn build(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result { @@ -752,7 +792,7 @@ impl PasetoBuilder<'_, V2, Public> { /// let public_key = Key::<32>::try_from("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2")?; /// let public_key = PasetoAsymmetricPublicKey::::from(&public_key); /// let footer = Footer::from("some footer"); - /// //sign a public V2 token + /// //sign a public V2 token /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) /// .set_claim(SubjectClaim::from("loyal subjects")) @@ -824,7 +864,7 @@ impl PasetoBuilder<'_, V3, Public> { /// )?; /// let public_key = PasetoAsymmetricPublicKey::::try_from(&public_key)?; /// let footer = Footer::from("some footer"); - /// let implicit_assertion = ImplicitAssertion::from("some assertion"); + /// let implicit_assertion = ImplicitAssertion::from("some assertion"); /// //sign a public V3 token /// let token = PasetoBuilder::::default() /// .set_claim(AudienceClaim::from("customers")) @@ -953,14 +993,37 @@ mod paseto_builder { use crate::prelude::*; use anyhow::Result; use std::convert::TryFrom; + + #[cfg(feature = "time")] use time::format_description::well_known::Rfc3339; + // Helper: RFC3339 string for now + N seconds (positive offset only). + // For past datetimes use a hardcoded string. + #[cfg(feature = "time")] + fn rfc3339_from_now_plus_secs(secs: i64) -> String { + let d = time::Duration::seconds(secs); + (time::OffsetDateTime::now_utc() + d) + .format(&Rfc3339) + .expect("format failed") + } + + #[cfg(feature = "chrono")] + fn rfc3339_from_now_plus_secs(secs: i64) -> String { + let d = chrono::Duration::seconds(secs); + (chrono::Utc::now() + d).to_rfc3339() + } + + // Helper: date string (YYYY-MM-DD) for now + N seconds. + fn date_from_rfc3339(s: &str) -> String { + iso8601::datetime(s).expect("iso8601 parse failed").date.to_string() + } + #[test] fn duplicate_top_level_claim_test() -> Result<()> { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; + let tomorrow = rfc3339_from_now_plus_secs(86400); //let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339(); @@ -987,7 +1050,7 @@ mod paseto_builder { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; + let tomorrow = rfc3339_from_now_plus_secs(86400); //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -1008,7 +1071,7 @@ mod paseto_builder { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; + let tomorrow = rfc3339_from_now_plus_secs(86400); //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -1025,9 +1088,7 @@ mod paseto_builder { .ok_or_else(|| PasetoClaimError::Unexpected(key.to_string()))?; let datetime = iso8601::datetime(val).unwrap(); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)) - .date() - .to_string(); + let tomorrow = date_from_rfc3339(&rfc3339_from_now_plus_secs(86400)); //the claimm should exist assert_eq!(key, "iat"); @@ -1060,7 +1121,7 @@ mod paseto_builder { let datetime = iso8601::datetime(val).unwrap(); - let now = time::OffsetDateTime::now_utc().date().to_string(); + let now = date_from_rfc3339(&rfc3339_from_now_plus_secs(0)); //the claimm should exist assert_eq!(key, "iat"); //date should be today @@ -1079,7 +1140,7 @@ mod paseto_builder { let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); //let in_4_days = (Utc::now() + Duration::days(4)).to_rfc3339(); - let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)).format(&Rfc3339)?; + let in_4_days = rfc3339_from_now_plus_secs(4 * 86400); //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -1097,10 +1158,7 @@ mod paseto_builder { let datetime = iso8601::datetime(val).unwrap(); - let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)) - .date() - .to_string(); - //let in_4_days = Utc::now() + Duration::days(4); + let in_4_days = date_from_rfc3339(&rfc3339_from_now_plus_secs(4 * 86400)); //the claimm should exist assert_eq!(key, "exp"); //date should be tomorrow @@ -1131,9 +1189,7 @@ mod paseto_builder { .ok_or_else(|| PasetoClaimError::Unexpected(key.to_string()))?; let datetime = iso8601::datetime(val).unwrap(); - let expires = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)) - .date() - .to_string(); + let expires = date_from_rfc3339(&rfc3339_from_now_plus_secs(3600)); //let tomorrow = Utc::now() + Duration::hours(1); //the claimm should exist diff --git a/src/prelude/paseto_parser.rs b/src/prelude/paseto_parser.rs index ff0d20a..3519084 100644 --- a/src/prelude/paseto_parser.rs +++ b/src/prelude/paseto_parser.rs @@ -1,6 +1,8 @@ use crate::generic::*; use core::marker::PhantomData; use serde_json::Value; + +#[cfg(feature = "time")] use time::format_description::well_known::Rfc3339; ///The `PasetoParser` validates and parses PASETO tokens. Created at compile time by specifying a PASETO version and purpose. /// @@ -118,13 +120,18 @@ impl<'a, Version, Purpose> PasetoParser<'a, Version, Purpose> { /// //let's get the value /// let val = value.as_str().ok_or(PasetoClaimError::Unexpected(key.to_string()))?; /// let datetime = iso8601::datetime(val).unwrap(); + /// # #[cfg(feature = "time")] /// let in_an_hour = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)) /// .time() /// .hour() /// .to_string(); + /// # #[cfg(feature = "chrono")] + /// # let in_an_hour = (chrono::Utc::now() + chrono::Duration::hours(1)) + /// # .format("%H") + /// # .to_string(); /// //the claimm should exist /// assert_eq!(key, "exp"); - /// //date should be today + /// //hour should match expected /// assert_eq!(datetime.time.hour.to_string(), in_an_hour); /// Ok(()) /// }) @@ -220,10 +227,19 @@ impl<'a, Version, Purpose> PasetoParser<'a, Version, Purpose> { if val.is_empty() { return Ok(()); } - let datetime = time::OffsetDateTime::parse(val, &Rfc3339) - .map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; - let now = time::OffsetDateTime::now_utc(); - if datetime <= now { + #[cfg(feature = "time")] + let expired: bool = { + let datetime = + time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + datetime <= time::OffsetDateTime::now_utc() + }; + #[cfg(feature = "chrono")] + let expired: bool = { + let datetime = + chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + datetime <= chrono::Utc::now() + }; + if expired { Err(PasetoClaimError::Expired) } else { Ok(()) @@ -345,14 +361,20 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> { if val.is_empty() { return Err(PasetoClaimError::Missing("exp".to_string())); } - //turn the value into a datetime - let datetime = - time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; - //get the current datetime - let now = time::OffsetDateTime::now_utc(); - - //here we do the actual validation check for the expiration claim - if datetime <= now { + #[cfg(feature = "time")] + let expired: bool = { + //turn the value into a datetime + let datetime = + time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + datetime <= time::OffsetDateTime::now_utc() + }; + #[cfg(feature = "chrono")] + let expired: bool = { + let datetime = + chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + datetime <= chrono::Utc::now() + }; + if expired { Err(PasetoClaimError::Expired) } else { Ok(()) @@ -366,18 +388,38 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> { return Ok(()); } //otherwise let's continue with the validation + #[cfg(feature = "time")] //turn the value into a datetime - let not_before_time = - time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; - //get the current datetime - let now = time::OffsetDateTime::now_utc(); - - //here we do the actual validation check for the not-before claim. - //RFC 7519 §4.1.5: token is valid when `now >= nbf`, so reject only when - //`now < nbf`. Using strict `<` here (not `<=`) so that a token with - //nbf == now is accepted as soon as its activation instant arrives. - if now < not_before_time { - Err(PasetoClaimError::UseBeforeAvailable(not_before_time.to_string())) + let not_before_str: Option = { + let not_before_time = + time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + //get the current datetime + let now = time::OffsetDateTime::now_utc(); + + //here we do the actual validation check for the not-before claim. + //RFC 7519 §4.1.5: token is valid when `now >= nbf`, so reject only when + //`now < nbf`. Using strict `<` here (not `<=`) so that a token with + //nbf == now is accepted as soon as its activation instant arrives. + if now < not_before_time { + Some(not_before_time.to_string()) + } else { + None + } + }; + #[cfg(feature = "chrono")] + let not_before_str: Option = { + let not_before_time = + chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + let now = chrono::Utc::now(); + if now < not_before_time { + Some(not_before_time.to_string()) + } else { + None + } + }; + //RFC 7519 §4.1.5: reject only when now < nbf (strict <, so nbf == now is accepted). + if let Some(not_before_str) = not_before_str { + Err(PasetoClaimError::UseBeforeAvailable(not_before_str)) } else { Ok(()) } @@ -777,8 +819,8 @@ impl<'a> PasetoParser<'a, V1, Public> { /// # Ok::<(),anyhow::Error>(()) ///``` #[deprecated( - since = "0.8.1", - note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." + since = "0.8.1", + note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." )] #[allow(deprecated)] pub fn parse( @@ -794,8 +836,8 @@ impl<'a> PasetoParser<'a, V1, Public> { /// /// See [`PasetoParser::parse_into`] for detailed documentation. #[deprecated( - since = "0.8.1", - note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." + since = "0.8.1", + note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code." )] #[allow(deprecated)] pub fn parse_into( @@ -1123,9 +1165,27 @@ mod paseto_parser_unit_tests { use crate::prelude::*; use anyhow::Result; - #[cfg(feature = "v2_local")] + + #[cfg(feature = "time")] use time::format_description::well_known::Rfc3339; + #[cfg(feature = "time")] + fn rfc3339_from_now_plus_secs(secs: i64) -> String { + let d = time::Duration::seconds(secs); + (time::OffsetDateTime::now_utc() + d) + .format(&Rfc3339) + .expect("format failed") + } + + #[cfg(feature = "chrono")] + fn rfc3339_from_now_plus_secs(secs: i64) -> String { + (chrono::Utc::now() + chrono::Duration::seconds(secs)).to_rfc3339() + } + + fn date_from_rfc3339(s: &str) -> String { + iso8601::datetime(s).expect("iso8601 parse failed").date.to_string() + } + #[cfg(feature = "v2_local")] #[test] fn usage_before_ready_test() -> Result<()> { @@ -1133,7 +1193,7 @@ mod paseto_parser_unit_tests { let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); //let not_before = Utc::now() + Duration::hours(1); - let not_before = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)).format(&Rfc3339)?; + let not_before = rfc3339_from_now_plus_secs(3600); //create a default builder let token = PasetoBuilder::::default() .set_claim(NotBeforeClaim::try_from(not_before)?) @@ -1154,7 +1214,7 @@ mod paseto_parser_unit_tests { let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); //we're going to set a token expiration date to 10 minutes ago - let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?; + let expired = rfc3339_from_now_plus_secs(-600); //create a default builder let token = PasetoBuilder::::default() @@ -1195,10 +1255,7 @@ mod paseto_parser_unit_tests { //The default parser must reject it. let result = PasetoParser::::default().parse(&token, &key); - assert!( - result.is_err(), - "default parser must reject a token with no exp claim", - ); + assert!(result.is_err(), "default parser must reject a token with no exp claim",); let err = format!("{}", result.unwrap_err()); assert!( err.contains("exp"), @@ -1219,7 +1276,7 @@ mod paseto_parser_unit_tests { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?; + let expired = rfc3339_from_now_plus_secs(-600); //create a default builder let token = PasetoBuilder::::default() .set_claim(ExpirationClaim::try_from(expired)?) @@ -1308,7 +1365,7 @@ mod paseto_parser_unit_tests { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; + let tomorrow = rfc3339_from_now_plus_secs(86400); //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -1325,9 +1382,7 @@ mod paseto_parser_unit_tests { let datetime = iso8601::datetime(val).unwrap(); //let tomorrow = Utc::now() + Duration::days(1); - let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)) - .date() - .to_string(); + let tomorrow = date_from_rfc3339(&rfc3339_from_now_plus_secs(86400)); //the claimm should exist assert_eq!(key, "iat"); //date should be tomorrow @@ -1359,7 +1414,7 @@ mod paseto_parser_unit_tests { let datetime = iso8601::datetime(val).unwrap(); //the claimm should exist - let now = time::OffsetDateTime::now_utc().date().to_string(); + let now = date_from_rfc3339(&rfc3339_from_now_plus_secs(0)); assert_eq!(key, "iat"); //date should be today assert_eq!(datetime.date.to_string(), now); @@ -1377,7 +1432,7 @@ mod paseto_parser_unit_tests { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)).format(&Rfc3339)?; + let in_4_days = rfc3339_from_now_plus_secs(4 * 86400); //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -1394,9 +1449,7 @@ mod paseto_parser_unit_tests { let datetime = iso8601::datetime(val).unwrap(); //let in_4_days = Utc::now() + Duration::days(4); - let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)) - .date() - .to_string(); + let in_4_days = date_from_rfc3339(&rfc3339_from_now_plus_secs(4 * 86400)); //the claimm should exist assert_eq!(key, "exp"); //date should be tomorrow @@ -1427,14 +1480,11 @@ mod paseto_parser_unit_tests { let datetime = iso8601::datetime(val).unwrap(); - let in_an_hour = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)) - .time() - .hour() - .to_string(); + let in_an_hour = date_from_rfc3339(&rfc3339_from_now_plus_secs(3600)); //the claimm should exist assert_eq!(key, "exp"); - //date should be today - assert_eq!(datetime.time.hour.to_string(), in_an_hour); + //date should be today (or tomorrow if we're within 1 hour of midnight) + assert_eq!(datetime.date.to_string(), in_an_hour); Ok(()) })