diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 553774bb46..ac532e3a72 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -5,7 +5,7 @@ on: branches: - main pull_request: - types: [ opened, synchronize, reopened, ready_for_review ] + types: [opened, synchronize, reopened, ready_for_review] branches: - main - 'epic/**' @@ -62,12 +62,12 @@ jobs: build-and-test: runs-on: ${{ matrix.os }} - needs: [ check-for-run-condition, check-for-modification ] + needs: [check-for-run-condition, check-for-modification] if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' && needs.check-for-modification.outputs.core-modified == 'true' }} strategy: fail-fast: false matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ubuntu-latest, macos-latest, windows-latest] include: - os: ubuntu-latest sccache-path: /home/runner/.cache/sccache @@ -127,14 +127,17 @@ jobs: # Build the library, tests, and examples without running them to avoid recompilation in the run tests step - name: Build with all features - run: cargo build --workspace --tests --examples --all-features --release + run: cargo build --workspace --tests --examples --release - name: Start iota sandbox if: matrix.os == 'ubuntu-latest' uses: './.github/actions/iota-sandbox/setup' - - name: Run tests - run: cargo test --workspace --all-features --release + - name: Run tests excluding `custom_time` feature + run: cargo test --workspace --release + + - name: Run tests with `custom_time` feature + run: cargo test --test custom_time --features="custom_time" - name: Run Rust examples # run examples only on ubuntu for now @@ -157,7 +160,7 @@ jobs: - name: Tear down iota sandbox if: matrix.os == 'ubuntu-latest' && always() uses: './.github/actions/iota-sandbox/tear-down' - + - name: Stop sccache uses: './.github/actions/rust/sccache/stop-sccache' with: @@ -178,7 +181,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest @@ -218,7 +221,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest @@ -229,7 +232,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: 16.x - + - name: Install JS dependencies run: npm ci working-directory: bindings/wasm @@ -266,7 +269,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index 0f7a8a34eb..f8aa615b28 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "The core traits and types for the identity-rs library." [dependencies] -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "random", "sha", "x25519", "std"] } multibase = { version = "0.9", default-features = false, features = ["std"] } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true, features = ["std"] } @@ -22,7 +21,7 @@ time = { version = "0.3.23", default-features = false, features = ["std", "serde url = { version = "2.4", default-features = false, features = ["serde"] } zeroize = { version = "1.6", default-features = false } -[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))'.dependencies] js-sys = { version = "0.3.55", default-features = false } [dev-dependencies] @@ -38,3 +37,11 @@ rustdoc-args = ["--cfg", "docsrs"] [lints] workspace = true + +[features] +# Enables a macro to provide a custom time (Timestamp::now_utc) implementation, see src/custom_time.rs +custom_time = [] + +[[test]] +name = "custom_time" +required-features = ["custom_time"] diff --git a/identity_core/src/common/ordered_set.rs b/identity_core/src/common/ordered_set.rs index b3650490ef..885342409b 100644 --- a/identity_core/src/common/ordered_set.rs +++ b/identity_core/src/common/ordered_set.rs @@ -488,7 +488,7 @@ mod tests { /// Produces a strategy for generating an ordered set together with two values according to the following algorithm: /// 1. Call `f` to get a pair of sets (x,y). /// 2. Toss a coin to decide whether to pick an element from x at random, or from y (if the chosen set is empty - /// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b. + /// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b. /// 4. Toss a coin to decide whether to swap the keys of a and b. /// 5. return (x,a,b) fn set_with_values(f: F) -> impl Strategy, T, T)> diff --git a/identity_core/src/common/timestamp.rs b/identity_core/src/common/timestamp.rs index 8de1832409..4f03db2cea 100644 --- a/identity_core/src/common/timestamp.rs +++ b/identity_core/src/common/timestamp.rs @@ -42,7 +42,10 @@ impl Timestamp { /// fractional seconds truncated. /// /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))] + #[cfg(all( + not(all(target_arch = "wasm32", not(target_os = "wasi"))), + not(feature = "custom_time") + ))] pub fn now_utc() -> Self { Self(truncate_fractional_seconds(OffsetDateTime::now_utc())) } @@ -51,7 +54,7 @@ impl Timestamp { /// fractional seconds truncated. /// /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))] pub fn now_utc() -> Self { let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64; let seconds: i64 = milliseconds_since_unix_epoch / 1000; @@ -59,6 +62,15 @@ impl Timestamp { Self::from_unix(seconds).expect("Timestamp failed to convert system datetime") } + /// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with + /// fractional seconds truncated. + /// + /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). + #[cfg(feature = "custom_time")] + pub fn now_utc() -> Self { + crate::custom_time::now_utc_custom() + } + /// Returns the `Timestamp` as an [RFC 3339](https://tools.ietf.org/html/rfc3339) `String`. pub fn to_rfc3339(&self) -> String { // expect is okay, constructors ensure RFC 3339 compatible timestamps. diff --git a/identity_core/src/custom_time.rs b/identity_core/src/custom_time.rs new file mode 100644 index 0000000000..ef509a19de --- /dev/null +++ b/identity_core/src/custom_time.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! An implementation of `now_utc` which calls out to an externally defined function. +use crate::common::Timestamp; + +/// Register a function to be invoked by `identity_core` in order to get a [Timestamp] representing +/// "now". +/// +/// ## Writing a custom `now_utc` implementation +/// +/// The function to register must have the same signature as +/// [`Timestamp::now_utc`](Timestamp::now_utc). The function can be defined +/// wherever you want, either in root crate or a dependent crate. +/// +/// For example, if we wanted a `static_now_utc` crate containing an +/// implementation that always returns the same timestamp, we would first depend on `identity_core` +/// (for the [`Timestamp`] type) in `static_now_utc/Cargo.toml`: +/// ```toml +/// [dependencies] +/// identity_core = "1" +/// ``` +/// Note that the crate containing this function does **not** need to enable the +/// `"custom_time"` Cargo feature. +/// +/// Next, in `static_now_utc/src/lib.rs`, we define our function: +/// ```rust +/// use identity_core::common::Timestamp; +/// +/// // Some fixed timestamp +/// const MY_FIXED_TIMESTAMP: i64 = 1724402964; +/// pub fn static_now_utc() -> Timestamp { +/// Timestamp::from_unix(MY_FIXED_TIMESTAMP).unwrap() +/// } +/// ``` +/// +/// ## Registering a custom `now_utc` implementation +/// +/// Functions can only be registered in the root binary crate. Attempting to +/// register a function in a non-root crate will result in a linker error. +/// This is similar to +/// [`#[panic_handler]`](https://doc.rust-lang.org/nomicon/panic-handler.html) or +/// [`#[global_allocator]`](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/global-allocators.html), +/// where helper crates define handlers/allocators but only the binary crate +/// actually _uses_ the functionality. +/// +/// To register the function, we first depend on `static_now_utc` _and_ +/// `identity_core` in `Cargo.toml`: +/// ```toml +/// [dependencies] +/// static_now_utc = "0.1" +/// identity_core = { version = "1", features = ["custom_time"] } +/// ``` +/// +/// Then, we register the function in `src/main.rs`: +/// ```rust +/// # mod static_now_utc { pub fn static_now_utc() -> Timestamp { unimplemented!() } } +/// +/// use identity_core::register_custom_now_utc; +/// use static_now_utc::static_now_utc; +/// +/// register_custom_now_utc!(static_now_utc); +/// ``` +/// +/// Now any user of `now_utc` (direct or indirect) on this target will use the +/// registered function. +#[macro_export] +macro_rules! register_custom_now_utc { + ($path:path) => { + const __GET_TIME_INTERNAL: () = { + // We use Rust ABI to be safe against potential panics in the passed function. + #[no_mangle] + unsafe fn __now_utc_custom() -> Timestamp { + // Make sure the passed function has the type of `now_utc_custom` + type F = fn() -> Timestamp; + let f: F = $path; + f() + } + }; + }; +} + +pub(crate) fn now_utc_custom() -> Timestamp { + extern "Rust" { + fn __now_utc_custom() -> Timestamp; + } + unsafe { __now_utc_custom() } +} diff --git a/identity_core/src/lib.rs b/identity_core/src/lib.rs index b915fcdeba..0e439441e4 100644 --- a/identity_core/src/lib.rs +++ b/identity_core/src/lib.rs @@ -1,7 +1,6 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#![forbid(unsafe_code)] #![doc = include_str!("./../README.md")] #![allow(clippy::upper_case_acronyms)] #![warn( @@ -19,9 +18,15 @@ #[doc(inline)] pub use serde_json::json; +#[forbid(unsafe_code)] pub mod common; +#[forbid(unsafe_code)] pub mod convert; +#[forbid(unsafe_code)] pub mod error; +#[cfg(feature = "custom_time")] +pub mod custom_time; + pub use self::error::Error; pub use self::error::Result; diff --git a/identity_core/tests/custom_time.rs b/identity_core/tests/custom_time.rs new file mode 100644 index 0000000000..9c700d523e --- /dev/null +++ b/identity_core/tests/custom_time.rs @@ -0,0 +1,18 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Timestamp; +use identity_core::register_custom_now_utc; + +const STATIC_TIME: i64 = 1724402964; // 2024-08-23T11:33:30+00:00 +pub fn static_now_utc() -> Timestamp { + Timestamp::from_unix(STATIC_TIME).unwrap() +} + +register_custom_now_utc!(static_now_utc); + +#[test] +fn should_use_registered_static_time() { + let timestamp = Timestamp::now_utc(); + assert_eq!(timestamp.to_unix(), STATIC_TIME) +} diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 6ce3c60b67..3f2a33f0a7 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -32,7 +32,7 @@ use crate::Result; /// This type is opinionated in the following ways: /// 1. Serialization tries to duplicate as little as possible between the required registered claims and the `vc` entry. /// 2. Only allows serializing/deserializing claims "exp, iss, nbf &/or iat, jti, sub and vc". Other custom properties -/// must be set in the `vc` entry. +/// must be set in the `vc` entry. #[derive(Serialize, Deserialize)] pub(crate) struct CredentialJwtClaims<'credential, T = Object> where diff --git a/identity_credential/src/domain_linkage/domain_linkage_validator.rs b/identity_credential/src/domain_linkage/domain_linkage_validator.rs index 24969c1c65..be67c96832 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_validator.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_validator.rs @@ -38,15 +38,15 @@ impl JwtDomainLinkageValidator { /// Validates the linkage between a domain and a DID. /// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification). /// - /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included - /// in the Domain Linkage Configuration. + /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included in the Domain Linkage + /// Configuration. /// * `configuration`: Domain Linkage Configuration fetched from the domain at "/.well-known/did-configuration.json". /// * `domain`: domain from which the Domain Linkage Configuration has been fetched. /// * `validation_options`: Further validation options to be applied on the Domain Linkage Credential. /// /// # Note: /// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format) - /// is supported. + /// is supported. /// - Only the Credential issued by `issuer` is verified. /// /// # Errors diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs index 4402283e1a..ed96643cb7 100644 --- a/identity_credential/src/revocation/status_list_2021/credential.rs +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -123,7 +123,7 @@ impl StatusList2021Credential { /// /// ## Note: /// - A revoked credential cannot ever be unrevoked and will lead to a - /// [`StatusList2021CredentialError::UnreversibleRevocation`]. + /// [`StatusList2021CredentialError::UnreversibleRevocation`]. /// - Trying to set `revoked_or_suspended` to `false` for an already valid credential will have no impact. pub fn set_credential_status( &mut self, diff --git a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs index a02b2cf56a..d6d97fcf6e 100644 --- a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs @@ -47,11 +47,11 @@ where /// # Warning /// /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the - /// credentials' subjects and the presentation holder. This can be done with - /// [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions). + /// credentials' subjects and the presentation holder. This can be done with + /// [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions). /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can - /// be trusted. This section contains more information on additional checks that should be carried out before and - /// after calling this method. + /// be trusted. This section contains more information on additional checks that should be carried out before and + /// after calling this method. /// /// ## The state of the supplied DID Documents. /// diff --git a/identity_credential/src/validator/sd_jwt/validator.rs b/identity_credential/src/validator/sd_jwt/validator.rs index 0eedf13bf5..e01985fa01 100644 --- a/identity_credential/src/validator/sd_jwt/validator.rs +++ b/identity_credential/src/validator/sd_jwt/validator.rs @@ -53,10 +53,10 @@ impl SdJwtCredentialValidator { /// /// # Warning /// * The key binding JWT is not validated. If needed, it must be validated separately using - /// `SdJwtValidator::validate_key_binding_jwt`. + /// `SdJwtValidator::validate_key_binding_jwt`. /// * The lack of an error returned from this method is in of itself not enough to conclude that the credential can be - /// trusted. This section contains more information on additional checks that should be carried out before and after - /// calling this method. + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// calling this method. /// /// ## The state of the issuer's DID Document /// The caller must ensure that `issuer` represents an up-to-date DID Document. diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 1b226f9585..2f6bcd593f 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -938,8 +938,8 @@ impl CoreDocument { /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to /// take place. /// - The JWS must be encoded according to the JWS compact serialization. - /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, - /// or set explicitly in the `options`. + /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, or + /// set explicitly in the `options`. // // NOTE: This is tested in `identity_storage` and `identity_credential`. pub fn verify_jws<'jws, T: JwsVerifier>( diff --git a/identity_jose/src/jwu/serde.rs b/identity_jose/src/jwu/serde.rs index cd80a1c949..e875da0e10 100644 --- a/identity_jose/src/jwu/serde.rs +++ b/identity_jose/src/jwu/serde.rs @@ -57,8 +57,7 @@ pub(crate) fn validate_jws_headers(protected: Option<&JwsHeader>, unprotected: O /// Validates that the "crit" parameter satisfies the following requirements: /// 1. It is integrity protected. /// 2. It is not encoded as an empty list. -/// 3. It does not contain any header parameters defined by the -/// JOSE JWS/JWA specifications. +/// 3. It does not contain any header parameters defined by the JOSE JWS/JWA specifications. /// 4. It's values are contained in the given `permitted` array. /// 5. All values in "crit" are present in at least one of the `protected` or `unprotected` headers. /// diff --git a/identity_jose/src/tests/rfc8037.rs b/identity_jose/src/tests/rfc8037.rs index aada7a7369..0d0904810f 100644 --- a/identity_jose/src/tests/rfc8037.rs +++ b/identity_jose/src/tests/rfc8037.rs @@ -49,21 +49,18 @@ fn test_rfc8037_ed25519() { .decode_compact_serialization(jws.as_bytes(), None) .and_then(|decoded| decoded.verify(&jws_verifier, &public)) .unwrap(); + let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { + JwsAlgorithm::EdDSA => ed25519::verify(input, key), + other => unimplemented!("{other}"), + }); - #[cfg(feature = "eddsa")] - { - let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { - JwsAlgorithm::EdDSA => ed25519::verify(input, key), - other => unimplemented!("{other}"), - }); + let decoder = Decoder::new(); + let token_with_default = decoder + .decode_compact_serialization(jws.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_signature_verifier, &public)) + .unwrap(); - let decoder = Decoder::new(); - let token_with_default = decoder - .decode_compact_serialization(jws.as_bytes(), None) - .and_then(|decoded| decoded.verify(&jws_signature_verifier, &public)) - .unwrap(); - assert_eq!(token, token_with_default); - } + assert_eq!(token, token_with_default); assert_eq!(token.protected, header); assert_eq!(token.claims, tv.payload.as_bytes()); } diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index b8ceffbc7f..eff86351be 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -301,10 +301,10 @@ mod iota_handler { /// /// # Note /// - /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all - /// previously added clients. - /// - This function does not validate the provided configuration. Ensure that the provided - /// network name corresponds with the client, possibly by using `client.network_name()`. + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added + /// clients. + /// - This function does not validate the provided configuration. Ensure that the provided network name corresponds + /// with the client, possibly by using `client.network_name()`. pub fn attach_multiple_iota_handlers(&mut self, clients: I) where CLI: IotaIdentityClientExt + Send + Sync + 'static,