Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Error::InvalidKey` for malformed DER key material.
- `examples/asymmetric_signing.rs` and `examples/sample_es256_ps256_tokens.rs`
demonstrating the new asymmetric algorithms and key formats.
- `VerificationOptions::now` sets the current time (Unix seconds) used by
`verify_claims` for the `exp` and `nbf` checks, so callers can inject a
clock. By default the system clock is used, as before.
- `VerificationOptions::leeway` allows a clock skew (seconds) in the `exp` and
`nbf` checks: tokens stay valid up to `leeway` seconds after `exp` and become
valid `leeway` seconds before `nbf`. Defaults to 0.

### Changed

Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ pub mod header;
pub mod token;
pub mod utils;

#[cfg(test)]
mod tests_clock;

pub use cat_claims::{
catalpn, catdpop, catgeoalt, catgeocoord, catgeoiso3166, cath, catif, catifdata, catm, catnip,
catpor, catr, catreplay, cattpk, cattprint, catu, catv,
Expand Down
77 changes: 77 additions & 0 deletions src/tests_clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Tests for the injectable verification clock

use crate::{
claims::RegisteredClaims,
header::Algorithm,
token::{Token, TokenBuilder, VerificationOptions},
Error,
};

const NBF: u64 = 1_000_000;
const EXP: u64 = 2_000_000;

fn token() -> Token {
TokenBuilder::new()
.algorithm(Algorithm::HmacSha256)
.registered_claims(
RegisteredClaims::new()
.with_not_before(NBF)
.with_expiration(EXP),
)
.sign(b"clock-test-key")
.unwrap()
}

#[test]
fn test_injected_clock_drives_exp_and_nbf() {
let token = token();
let at = |now| token.verify_claims(&VerificationOptions::new().now(now));

assert!(matches!(at(NBF - 1), Err(Error::NotYetValid)));
assert!(at(NBF).is_ok());
assert!(at(EXP - 1).is_ok());
assert!(matches!(at(EXP), Err(Error::Expired)));
}

#[test]
fn test_system_clock_is_the_default() {
// EXP is in 1970, so the system clock sees the token as expired
assert_eq!(VerificationOptions::new().now, None);
assert!(matches!(
token().verify_claims(&VerificationOptions::new()),
Err(Error::Expired)
));
}

#[test]
fn test_leeway_extends_exp_and_nbf_in_the_permissive_direction() {
let token = token();
let at = |now, leeway| token.verify_claims(&VerificationOptions::new().now(now).leeway(leeway));

// exp: valid until leeway seconds past exp
assert!(at(EXP + 29, 30).is_ok());
assert!(matches!(at(EXP + 30, 30), Err(Error::Expired)));
// nbf: valid from leeway seconds before nbf
assert!(at(NBF - 30, 30).is_ok());
assert!(matches!(at(NBF - 31, 30), Err(Error::NotYetValid)));
// No leeway by default
assert_eq!(VerificationOptions::new().leeway, 0);
assert!(matches!(at(EXP, 0), Err(Error::Expired)));
}

#[test]
fn test_leeway_saturates() {
let token = TokenBuilder::new()
.algorithm(Algorithm::HmacSha256)
.registered_claims(
RegisteredClaims::new()
.with_not_before(u64::MAX)
.with_expiration(u64::MAX),
)
.sign(b"clock-test-key")
.unwrap();
let options = VerificationOptions::new()
.now(u64::MAX - 1)
.leeway(u64::MAX);
assert!(token.verify_claims(&options).is_ok());
}
35 changes: 32 additions & 3 deletions src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,13 @@ impl Token {

/// Verify the token claims
pub fn verify_claims(&self, options: &VerificationOptions) -> Result<(), Error> {
let now = current_timestamp();
let now = options.now.unwrap_or_else(current_timestamp);

// Check expiration
if options.verify_exp {
if let Some(exp) = self.claims.registered.exp {
if now >= exp {
// Tolerate clock skew: still valid up to `leeway` seconds past exp
if now >= exp.saturating_add(options.leeway) {
return Err(Error::Expired);
}
} else if options.require_exp {
Expand All @@ -237,7 +238,8 @@ impl Token {
// Check not before
if options.verify_nbf {
if let Some(nbf) = self.claims.registered.nbf {
if now < nbf {
// Tolerate clock skew: already valid up to `leeway` seconds before nbf
if now.saturating_add(options.leeway) < nbf {
return Err(Error::NotYetValid);
}
}
Expand Down Expand Up @@ -1139,6 +1141,11 @@ pub struct VerificationOptions {
pub require_exp: bool,
/// Verify not before claim
pub verify_nbf: bool,
/// Current time (Unix seconds) for the exp and nbf checks; `None` uses the
/// system clock
pub now: Option<u64>,
/// Allowed clock skew (seconds) for the exp and nbf checks
pub leeway: u64,
/// Expected issuer
pub expected_issuer: Option<String>,
/// Require issuer claim
Expand Down Expand Up @@ -1174,6 +1181,8 @@ impl VerificationOptions {
verify_exp: true,
require_exp: false,
verify_nbf: true,
now: None,
leeway: 0,
expected_issuer: None,
require_iss: false,
expected_audience: None,
Expand Down Expand Up @@ -1208,6 +1217,26 @@ impl VerificationOptions {
self
}

/// Set the current time (Unix seconds) used for the exp and nbf checks
///
/// By default [`Token::verify_claims`] reads the system clock. Set this to
/// use an injected clock instead, for example in deterministic simulation
/// or tests, or when the caller already holds a trusted time source.
pub fn now(mut self, timestamp: u64) -> Self {
self.now = Some(timestamp);
self
}

/// Set the allowed clock skew (seconds) for the exp and nbf checks
///
/// A token stays valid until `leeway` seconds after its `exp`, and becomes
/// valid `leeway` seconds before its `nbf`, to tolerate small clock
/// differences between issuer and verifier. Defaults to 0.
pub fn leeway(mut self, seconds: u64) -> Self {
self.leeway = seconds;
self
}

/// Set expected issuer
pub fn expected_issuer<S: Into<String>>(mut self, issuer: S) -> Self {
self.expected_issuer = Some(issuer.into());
Expand Down
Loading