Skip to content

Commit 842f483

Browse files
Frederik Rothenbergeritsyaasir
andauthored
Add feature to support custom now_utc implementations (#1397)
* Add feature to support custom `now_utc` implementations This PR adds a feature to `identity_core` to allow specifying a custom function to get the current time (`Timestamp::now_utc`). The feature is disabled by default. Closes #1391. * Formatting * Fix wrong comment * chore: clippy fixes and fmt * chore: clippy fixes and fmt * Allow compilation for target wasm32-unknown-unknown without js-sys Also removes the unused dependency on `iota-crypto` (which also had a dependency on `js-sys` through the `random` feature). * chore(ci): Fix CI actions; add random feature to iota crypto --------- Co-authored-by: Yasir <yasirshariffa@gmail.com>
1 parent 13f9987 commit 842f483

File tree

18 files changed

+193
-53
lines changed

18 files changed

+193
-53
lines changed

.github/workflows/build-and-test.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,17 @@ jobs:
127127

128128
# Build the library, tests, and examples without running them to avoid recompilation in the run tests step
129129
- name: Build with all features
130-
run: cargo build --workspace --tests --examples --all-features --release
130+
run: cargo build --workspace --tests --examples --release
131131

132132
- name: Start iota sandbox
133133
if: matrix.os == 'ubuntu-latest'
134134
uses: './.github/actions/iota-sandbox/setup'
135135

136-
- name: Run tests
137-
run: cargo test --workspace --all-features --release
136+
- name: Run tests excluding `custom_time` feature
137+
run: cargo test --workspace --release
138+
139+
- name: Run tests with `custom_time` feature
140+
run: cargo test --test custom_time --features="custom_time"
138141

139142
- name: Run Rust examples
140143
# run examples only on ubuntu for now
@@ -157,7 +160,7 @@ jobs:
157160
- name: Tear down iota sandbox
158161
if: matrix.os == 'ubuntu-latest' && always()
159162
uses: './.github/actions/iota-sandbox/tear-down'
160-
163+
161164
- name: Stop sccache
162165
uses: './.github/actions/rust/sccache/stop-sccache'
163166
with:

identity_core/Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ rust-version.workspace = true
1212
description = "The core traits and types for the identity-rs library."
1313

1414
[dependencies]
15-
iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "random", "sha", "x25519", "std"] }
1615
multibase = { version = "0.9", default-features = false, features = ["std"] }
1716
serde = { workspace = true, features = ["std"] }
1817
serde_json = { workspace = true, features = ["std"] }
@@ -22,7 +21,7 @@ time = { version = "0.3.23", default-features = false, features = ["std", "serde
2221
url = { version = "2.4", default-features = false, features = ["serde"] }
2322
zeroize = { version = "1.6", default-features = false }
2423

25-
[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies]
24+
[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))'.dependencies]
2625
js-sys = { version = "0.3.55", default-features = false }
2726

2827
[dev-dependencies]
@@ -38,3 +37,11 @@ rustdoc-args = ["--cfg", "docsrs"]
3837

3938
[lints]
4039
workspace = true
40+
41+
[features]
42+
# Enables a macro to provide a custom time (Timestamp::now_utc) implementation, see src/custom_time.rs
43+
custom_time = []
44+
45+
[[test]]
46+
name = "custom_time"
47+
required-features = ["custom_time"]

identity_core/src/common/ordered_set.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ mod tests {
488488
/// Produces a strategy for generating an ordered set together with two values according to the following algorithm:
489489
/// 1. Call `f` to get a pair of sets (x,y).
490490
/// 2. Toss a coin to decide whether to pick an element from x at random, or from y (if the chosen set is empty
491-
/// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b.
491+
/// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b.
492492
/// 4. Toss a coin to decide whether to swap the keys of a and b.
493493
/// 5. return (x,a,b)
494494
fn set_with_values<F, T, U>(f: F) -> impl Strategy<Value = (OrderedSet<T>, T, T)>

identity_core/src/common/timestamp.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ impl Timestamp {
4242
/// fractional seconds truncated.
4343
///
4444
/// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
45-
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))]
45+
#[cfg(all(
46+
not(all(target_arch = "wasm32", not(target_os = "wasi"))),
47+
not(feature = "custom_time")
48+
))]
4649
pub fn now_utc() -> Self {
4750
Self(truncate_fractional_seconds(OffsetDateTime::now_utc()))
4851
}
@@ -51,14 +54,23 @@ impl Timestamp {
5154
/// fractional seconds truncated.
5255
///
5356
/// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
54-
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
57+
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))]
5558
pub fn now_utc() -> Self {
5659
let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64;
5760
let seconds: i64 = milliseconds_since_unix_epoch / 1000;
5861
// expect is okay, we assume the current time is between 0AD and 9999AD
5962
Self::from_unix(seconds).expect("Timestamp failed to convert system datetime")
6063
}
6164

65+
/// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with
66+
/// fractional seconds truncated.
67+
///
68+
/// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production).
69+
#[cfg(feature = "custom_time")]
70+
pub fn now_utc() -> Self {
71+
crate::custom_time::now_utc_custom()
72+
}
73+
6274
/// Returns the `Timestamp` as an [RFC 3339](https://tools.ietf.org/html/rfc3339) `String`.
6375
pub fn to_rfc3339(&self) -> String {
6476
// expect is okay, constructors ensure RFC 3339 compatible timestamps.

identity_core/src/custom_time.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! An implementation of `now_utc` which calls out to an externally defined function.
5+
use crate::common::Timestamp;
6+
7+
/// Register a function to be invoked by `identity_core` in order to get a [Timestamp] representing
8+
/// "now".
9+
///
10+
/// ## Writing a custom `now_utc` implementation
11+
///
12+
/// The function to register must have the same signature as
13+
/// [`Timestamp::now_utc`](Timestamp::now_utc). The function can be defined
14+
/// wherever you want, either in root crate or a dependent crate.
15+
///
16+
/// For example, if we wanted a `static_now_utc` crate containing an
17+
/// implementation that always returns the same timestamp, we would first depend on `identity_core`
18+
/// (for the [`Timestamp`] type) in `static_now_utc/Cargo.toml`:
19+
/// ```toml
20+
/// [dependencies]
21+
/// identity_core = "1"
22+
/// ```
23+
/// Note that the crate containing this function does **not** need to enable the
24+
/// `"custom_time"` Cargo feature.
25+
///
26+
/// Next, in `static_now_utc/src/lib.rs`, we define our function:
27+
/// ```rust
28+
/// use identity_core::common::Timestamp;
29+
///
30+
/// // Some fixed timestamp
31+
/// const MY_FIXED_TIMESTAMP: i64 = 1724402964;
32+
/// pub fn static_now_utc() -> Timestamp {
33+
/// Timestamp::from_unix(MY_FIXED_TIMESTAMP).unwrap()
34+
/// }
35+
/// ```
36+
///
37+
/// ## Registering a custom `now_utc` implementation
38+
///
39+
/// Functions can only be registered in the root binary crate. Attempting to
40+
/// register a function in a non-root crate will result in a linker error.
41+
/// This is similar to
42+
/// [`#[panic_handler]`](https://doc.rust-lang.org/nomicon/panic-handler.html) or
43+
/// [`#[global_allocator]`](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/global-allocators.html),
44+
/// where helper crates define handlers/allocators but only the binary crate
45+
/// actually _uses_ the functionality.
46+
///
47+
/// To register the function, we first depend on `static_now_utc` _and_
48+
/// `identity_core` in `Cargo.toml`:
49+
/// ```toml
50+
/// [dependencies]
51+
/// static_now_utc = "0.1"
52+
/// identity_core = { version = "1", features = ["custom_time"] }
53+
/// ```
54+
///
55+
/// Then, we register the function in `src/main.rs`:
56+
/// ```rust
57+
/// # mod static_now_utc { pub fn static_now_utc() -> Timestamp { unimplemented!() } }
58+
///
59+
/// use identity_core::register_custom_now_utc;
60+
/// use static_now_utc::static_now_utc;
61+
///
62+
/// register_custom_now_utc!(static_now_utc);
63+
/// ```
64+
///
65+
/// Now any user of `now_utc` (direct or indirect) on this target will use the
66+
/// registered function.
67+
#[macro_export]
68+
macro_rules! register_custom_now_utc {
69+
($path:path) => {
70+
const __GET_TIME_INTERNAL: () = {
71+
// We use Rust ABI to be safe against potential panics in the passed function.
72+
#[no_mangle]
73+
unsafe fn __now_utc_custom() -> Timestamp {
74+
// Make sure the passed function has the type of `now_utc_custom`
75+
type F = fn() -> Timestamp;
76+
let f: F = $path;
77+
f()
78+
}
79+
};
80+
};
81+
}
82+
83+
pub(crate) fn now_utc_custom() -> Timestamp {
84+
extern "Rust" {
85+
fn __now_utc_custom() -> Timestamp;
86+
}
87+
unsafe { __now_utc_custom() }
88+
}

identity_core/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2020-2021 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4-
#![forbid(unsafe_code)]
54
#![doc = include_str!("./../README.md")]
65
#![allow(clippy::upper_case_acronyms)]
76
#![warn(
@@ -19,9 +18,15 @@
1918
#[doc(inline)]
2019
pub use serde_json::json;
2120

21+
#[forbid(unsafe_code)]
2222
pub mod common;
23+
#[forbid(unsafe_code)]
2324
pub mod convert;
25+
#[forbid(unsafe_code)]
2426
pub mod error;
2527

28+
#[cfg(feature = "custom_time")]
29+
pub mod custom_time;
30+
2631
pub use self::error::Error;
2732
pub use self::error::Result;

identity_core/tests/custom_time.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use identity_core::common::Timestamp;
5+
use identity_core::register_custom_now_utc;
6+
7+
const STATIC_TIME: i64 = 1724402964; // 2024-08-23T11:33:30+00:00
8+
pub fn static_now_utc() -> Timestamp {
9+
Timestamp::from_unix(STATIC_TIME).unwrap()
10+
}
11+
12+
register_custom_now_utc!(static_now_utc);
13+
14+
#[test]
15+
fn should_use_registered_static_time() {
16+
let timestamp = Timestamp::now_utc();
17+
assert_eq!(timestamp.to_unix(), STATIC_TIME)
18+
}

identity_credential/src/credential/jwt_serialization.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::Result;
3232
/// This type is opinionated in the following ways:
3333
/// 1. Serialization tries to duplicate as little as possible between the required registered claims and the `vc` entry.
3434
/// 2. Only allows serializing/deserializing claims "exp, iss, nbf &/or iat, jti, sub and vc". Other custom properties
35-
/// must be set in the `vc` entry.
35+
/// must be set in the `vc` entry.
3636
#[derive(Serialize, Deserialize)]
3737
pub(crate) struct CredentialJwtClaims<'credential, T = Object>
3838
where

identity_credential/src/domain_linkage/domain_linkage_validator.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ impl<V: JwsVerifier> JwtDomainLinkageValidator<V> {
3838
/// Validates the linkage between a domain and a DID.
3939
/// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification).
4040
///
41-
/// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included
42-
/// in the Domain Linkage Configuration.
41+
/// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included in the Domain Linkage
42+
/// Configuration.
4343
/// * `configuration`: Domain Linkage Configuration fetched from the domain at "/.well-known/did-configuration.json".
4444
/// * `domain`: domain from which the Domain Linkage Configuration has been fetched.
4545
/// * `validation_options`: Further validation options to be applied on the Domain Linkage Credential.
4646
///
4747
/// # Note:
4848
/// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format)
49-
/// is supported.
49+
/// is supported.
5050
/// - Only the Credential issued by `issuer` is verified.
5151
///
5252
/// # Errors

identity_credential/src/revocation/status_list_2021/credential.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl StatusList2021Credential {
123123
///
124124
/// ## Note:
125125
/// - A revoked credential cannot ever be unrevoked and will lead to a
126-
/// [`StatusList2021CredentialError::UnreversibleRevocation`].
126+
/// [`StatusList2021CredentialError::UnreversibleRevocation`].
127127
/// - Trying to set `revoked_or_suspended` to `false` for an already valid credential will have no impact.
128128
pub fn set_credential_status(
129129
&mut self,

0 commit comments

Comments
 (0)