From f25c5932398367dd0c3eab9dcaeacaabf9c5db97 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Fri, 24 May 2024 12:38:12 +0200 Subject: [PATCH] Add ZK BBS+-based selectively disclosable credentials (JPT) (#1355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support BBS+ and JWP (#1285) * merge main * Wasm bindings for Jpt credentials * JPT presentation bindings * docs * jsonprooftoken payloads * Refactor `RevocationTimeframeStatus` to align with other setups (#1340) * refactor `RevocationTimeframeStatus` to other setups * fix smaller typos * binding coverage for jsonprooftoken * Use latest releases of zkryptium/json-proof-token and add new BLS key representation (#1339) * update zkryptium/json-proof-token deps and new BLS key representation * minor fix * Use zkryptium for cryptographic operations inside Memstore (#1351) * update zkryptium/json-proof-token deps and new BLS key representation * minor fix * use zkryptium for crypto operations and JPT for serialization * fix format * Feat/jpt bbs+ sd stronghold impl (#1354) * Implement JwkStorageExt for StrongholdStorage * reorganize code * persist changes to stronghold when creating bbs+ keypair, clippy, fmt * feature gate * zkp wasm example * zkp_revocation wasm example * wasm bindings * fix docs * rename JwkStorageExt to JwkStorageBbsPlusExt * JwkStorageBbsPlusExt impl refactor for Stronghold, MemStore, WasmStore * Squashed commit of the following: commit 30c9bf2458fd2e202e7ace71c693e08a3bac8d9c Author: Foorack / Max Faxälv Date: Tue Apr 2 10:32:48 2024 +0200 inherit `repository` in identity_verification (#1348) commit 1e9c9a31257a0f430cb9acd22d2911e949137453 Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed Mar 27 15:35:29 2024 +0100 Release wasm-v1.2.0 (#1345) commit 84a630dbf82376d7b6abac8acedfa99acc47bd60 Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed Mar 27 15:32:19 2024 +0100 Release v1.2.0 (#1347) commit 1aba4b5fb575936dd59ac6fb6be0e29b866a5a51 Author: Eike Haß Date: Wed Mar 27 13:13:27 2024 +0100 removed dev_dep version commit 0352b840f0ef0b8f57b343151b9d6ee08c716f74 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Wed Mar 27 10:44:43 2024 +0100 Support %-encoded characters in DID method id (#1303) commit e68538f95787a73ec9ae3d8fdf0746b61c6910db Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 26 11:58:35 2024 +0100 gRPC bindings (#1264) commit e53561e3b8dabc9ec80653d21c459d8f0205ad40 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 26 11:18:14 2024 +0100 allow large result err variants (#1342) commit 4a144a36990f3318e869c9192356a7ec06f10d54 Author: Eike Haß Date: Tue Mar 19 09:51:52 2024 +0100 fix readme links (#1336) commit 0af29fc8a630c0c698bc745f9434fab69320aa74 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Mon Mar 18 17:16:57 2024 +0100 Feat/custom verification method (#1334) * Add support for arbitrary (custom) verification method data * wasm bindings * custom method type + wasm * workaround serde's issue * Update bindings/wasm/src/verification/wasm_method_data.rs Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> * review comments * fmt * review comment --------- Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> commit edb91501e9ec933471ea4ff9b416e19273c02082 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 12 14:45:04 2024 +0100 use latest release of sd-jwt-payload (#1333) * use latest release of sd-jwt-payload * make clippy happy commit 0794379be3c18894745e5acad09488bdb3c773c6 Author: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Wed Mar 6 14:16:00 2024 +0100 Wasm bindings for `BlockChainAccountId` verification method. (#1326) commit 59d38f77e8460c1b5da55d751eec0cb88f315d9d Author: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Wed Mar 6 10:56:23 2024 +0100 Add constructor for VerificationMethod in TS (#1321) * clippy * fmt * add stronghold bbs+ tests * review comments * add license header * fix wasm bindings * Persist Stronghold's changes only when its handle is dropped * Fix StrongholdStorage::get_public_key * rename stronghold_jwk_storage_ext * Add inx-faucet profile in CI * change stronghold crate's structure, revert persist changes on drop * review comments * Update identity_credential/src/presentation/jwp_presentation_builder.rs Co-authored-by: wulfraem * fix wasm bindings * expose stronghold's key types * revert last commit * Add "Fondazione Links" to license header * Squashed commit of the following: commit 9abdb3868d76ccb39da2145346e201640448870a Author: Sven Date: Tue May 14 09:16:09 2024 +0200 Add EcDSA verifier (#1353) * add ecdsa verifier * add identity_ecdsa_verifier to workspace, add license headers * Update identity_ecdsa_verifier/Cargo.toml Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256k1.rs Co-authored-by: wulfraem * Update identity_ecdsa_verifier/Cargo.toml Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256k1.rs Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256r1.rs Co-authored-by: wulfraem * add feedback * add OpenSSL installation to windows runner in CI * update license headers and authors for ecdsa verifier * update license template to allow multiple contributors --------- Co-authored-by: Sebastian Wolfram commit 149bfac98e8d9d8ca3d890e413291e384447c62b Author: wulfraem Date: Mon May 13 10:44:09 2024 +0200 Fix findings after clippy update (#1365) * fix clippy findings * fix formatting * refactor .clone_into calls into .to_string * fix previous edit * disable empty_docs for wasm binding for now * fix missing newline * disable self update from rust setup in ci for now * update self update skip to skip only for windows build commit 51aedd51be086e333744b020e867c0348833a083 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Apr 30 16:16:36 2024 +0200 Use STRONGHOLD_PWD_FILE env variable to pass stronghold's password (#1363) commit edec26c18782ad75a20ca6bebd7c66959eadb91d Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Apr 30 15:40:55 2024 +0200 Arbitrary data signing service (#1350) commit f59e75a57df05971aef00549c8880b83c6600f2b Author: Eike Haß Date: Tue Apr 30 15:34:40 2024 +0200 Fix dockerhub workflow (#1343) commit 993cfec8a698f668f2891f46e6c94f2584c50c05 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Fri Apr 26 13:39:29 2024 +0200 add inx-faucet profile (#1356) * update stronghold and sdk --------- Co-authored-by: Alberto Solavagione Co-authored-by: wulfraem --- Cargo.toml | 2 + bindings/grpc/Cargo.toml | 2 +- bindings/wasm/Cargo.toml | 4 +- bindings/wasm/docs/api-reference.md | 1664 +++++++++++++++-- .../wasm/examples/src/1_advanced/8_zkp.ts | 226 +++ .../src/1_advanced/9_zkp_revocation.ts | 281 +++ bindings/wasm/examples/src/main.ts | 6 + bindings/wasm/examples/src/tests/8_zkp.ts | 8 + .../examples/src/tests/9_zkp_revocation.ts | 8 + bindings/wasm/lib/jwk_storage.ts | 23 +- bindings/wasm/package.json | 5 +- bindings/wasm/src/common/types.rs | 6 + bindings/wasm/src/credential/jpt.rs | 45 + .../decoded_jpt_credential.rs | 52 + .../jpt_credential_validation_options.rs | 80 + .../jpt_credential_validator.rs | 33 + .../jpt_credential_validator_utils.rs | 102 + .../jwp_credential_options.rs | 49 + .../jwp_verification_options.rs | 48 + .../jpt_credential_validator/mod.rs | 16 + .../decoded_jpt_presentation.rs | 51 + .../jpt_presentation_validation_options.rs | 64 + .../jpt_presentation_validator.rs | 41 + .../jpt_presentation_validator_utils.rs | 44 + .../jwp_presentation_options.rs | 37 + .../jpt_presentiation_validation/mod.rs | 14 + bindings/wasm/src/credential/mod.rs | 6 + .../wasm/src/credential/revocation/mod.rs | 1 + .../revocation/validity_timeframe_2024/mod.rs | 6 + .../validity_timeframe_2024/status.rs | 75 + bindings/wasm/src/did/wasm_core_document.rs | 3 + bindings/wasm/src/error.rs | 2 + bindings/wasm/src/iota/iota_document.rs | 143 +- bindings/wasm/src/jpt/encoding.rs | 29 + .../wasm/src/jpt/issuer_protected_header.rs | 52 + bindings/wasm/src/jpt/jpt_claims.rs | 31 + bindings/wasm/src/jpt/jwp_issued.rs | 50 + .../wasm/src/jpt/jwp_presentation_builder.rs | 83 + bindings/wasm/src/jpt/mod.rs | 20 + bindings/wasm/src/jpt/payload.rs | 151 ++ .../src/jpt/presentation_protected_header.rs | 86 + bindings/wasm/src/jpt/proof_algorithm.rs | 52 + bindings/wasm/src/lib.rs | 1 + .../storage/jpt_timeframe_revocation_ext.rs | 69 + bindings/wasm/src/storage/jwk_storage.rs | 3 + .../src/storage/jwk_storage_bbs_plus_ext.rs | 132 ++ bindings/wasm/src/storage/mod.rs | 3 + examples/0_basic/7_revoke_vc.rs | 2 + examples/1_advanced/10_zkp_revocation.rs | 534 ++++++ examples/1_advanced/9_zkp.rs | 260 +++ examples/Cargo.toml | 13 +- identity_credential/Cargo.toml | 10 +- .../src/credential/credential.rs | 12 + identity_credential/src/credential/jpt.rs | 33 + .../src/credential/jwp_credential_options.rs | 28 + .../src/credential/jwt_serialization.rs | 53 + identity_credential/src/credential/mod.rs | 10 + .../credential/revocation_bitmap_status.rs | 2 +- identity_credential/src/error.rs | 8 + .../presentation/jwp_presentation_builder.rs | 124 ++ .../presentation/jwp_presentation_options.rs | 33 + identity_credential/src/presentation/mod.rs | 8 + identity_credential/src/revocation/mod.rs | 5 + .../revocation/validity_timeframe_2024/mod.rs | 8 + .../revocation_timeframe_status.rs | 220 +++ .../decoded_jpt_credential.rs | 19 + .../jpt_credential_validation_options.rs | 87 + .../jpt_credential_validator.rs | 225 +++ .../jpt_credential_validator_utils.rs | 242 +++ .../jpt_credential_validation/mod.rs | 12 + .../decoded_jpt_presentation.rs | 22 + .../jpt_presentation_validation_options.rs | 40 + .../jpt_presentation_validator.rs | 226 +++ .../jpt_presentation_validator_utils.rs | 99 + .../jpt_presentation_validation/mod.rs | 12 + .../jwt_credential_validation/error.rs | 12 + identity_credential/src/validator/mod.rs | 8 + .../src/document/core_document.rs | 2 +- .../verifiable/jwp_verification_options.rs | 36 + identity_document/src/verifiable/mod.rs | 2 + identity_iota/Cargo.toml | 5 +- identity_iota/src/lib.rs | 17 +- identity_iota_core/Cargo.toml | 2 +- identity_jose/Cargo.toml | 1 + identity_jose/src/jwk/curve/bls.rs | 43 + identity_jose/src/jwk/curve/mod.rs | 2 + identity_jose/src/jwk/jwk_ext.rs | 162 ++ identity_jose/src/jwk/key_operation.rs | 8 + identity_jose/src/jwk/key_params.rs | 13 + identity_jose/src/jwk/key_use.rs | 4 + identity_jose/src/jwk/mod.rs | 1 + identity_resolver/Cargo.toml | 2 +- identity_storage/Cargo.toml | 7 +- identity_storage/src/key_storage/bls.rs | 203 ++ .../key_storage/jwk_storage_bbs_plus_ext.rs | 40 + .../src/key_storage/key_storage_error.rs | 4 + identity_storage/src/key_storage/memstore.rs | 155 +- identity_storage/src/key_storage/mod.rs | 26 +- identity_storage/src/lib.rs | 2 +- identity_storage/src/storage/error.rs | 10 + .../src/storage/jwk_document_ext.rs | 26 +- .../src/storage/jwp_document_ext.rs | 362 ++++ identity_storage/src/storage/mod.rs | 11 + .../src/storage/timeframe_revocation_ext.rs | 198 ++ identity_stronghold/Cargo.toml | 11 +- identity_stronghold/src/lib.rs | 8 +- identity_stronghold/src/storage/mod.rs | 163 ++ .../{ => storage}/stronghold_jwk_storage.rs | 224 +-- .../stronghold_jwk_storage_bbs_plus_ext.rs | 174 ++ .../src/{ => storage}/stronghold_key_id.rs | 2 +- .../src/stronghold_key_type.rs | 109 ++ identity_stronghold/src/tests/mod.rs | 1 + identity_stronghold/src/tests/test_bbs_ext.rs | 93 + .../src/tests/test_jwk_storage.rs | 5 +- identity_stronghold/src/utils.rs | 87 + 115 files changed, 8084 insertions(+), 413 deletions(-) create mode 100644 bindings/wasm/examples/src/1_advanced/8_zkp.ts create mode 100644 bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts create mode 100644 bindings/wasm/examples/src/tests/8_zkp.ts create mode 100644 bindings/wasm/examples/src/tests/9_zkp_revocation.ts create mode 100644 bindings/wasm/src/credential/jpt.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/mod.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs create mode 100644 bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs create mode 100644 bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs create mode 100644 bindings/wasm/src/jpt/encoding.rs create mode 100644 bindings/wasm/src/jpt/issuer_protected_header.rs create mode 100644 bindings/wasm/src/jpt/jpt_claims.rs create mode 100644 bindings/wasm/src/jpt/jwp_issued.rs create mode 100644 bindings/wasm/src/jpt/jwp_presentation_builder.rs create mode 100644 bindings/wasm/src/jpt/mod.rs create mode 100644 bindings/wasm/src/jpt/payload.rs create mode 100644 bindings/wasm/src/jpt/presentation_protected_header.rs create mode 100644 bindings/wasm/src/jpt/proof_algorithm.rs create mode 100644 bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs create mode 100644 bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs create mode 100644 examples/1_advanced/10_zkp_revocation.rs create mode 100644 examples/1_advanced/9_zkp.rs create mode 100644 identity_credential/src/credential/jpt.rs create mode 100644 identity_credential/src/credential/jwp_credential_options.rs create mode 100644 identity_credential/src/presentation/jwp_presentation_builder.rs create mode 100644 identity_credential/src/presentation/jwp_presentation_options.rs create mode 100644 identity_credential/src/revocation/validity_timeframe_2024/mod.rs create mode 100644 identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/mod.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/mod.rs create mode 100644 identity_document/src/verifiable/jwp_verification_options.rs create mode 100644 identity_jose/src/jwk/curve/bls.rs create mode 100644 identity_jose/src/jwk/jwk_ext.rs create mode 100644 identity_storage/src/key_storage/bls.rs create mode 100644 identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs create mode 100644 identity_storage/src/storage/jwp_document_ext.rs create mode 100644 identity_storage/src/storage/timeframe_revocation_ext.rs create mode 100644 identity_stronghold/src/storage/mod.rs rename identity_stronghold/src/{ => storage}/stronghold_jwk_storage.rs (50%) create mode 100644 identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs rename identity_stronghold/src/{ => storage}/stronghold_key_id.rs (98%) create mode 100644 identity_stronghold/src/stronghold_key_type.rs create mode 100644 identity_stronghold/src/tests/test_bbs_ext.rs create mode 100644 identity_stronghold/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 25e3491f1c..a0375aa810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } +json-proof-token = { version = "0.3.5" } +zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] } [workspace.package] authors = ["IOTA Stiftung"] diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index f594dc56d4..2b542712db 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -22,7 +22,7 @@ futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch", "status-list-2021"] } identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] } -iota-sdk = { version = "1.1.2", features = ["stronghold"] } +iota-sdk = { version = "1.1.5", features = ["stronghold"] } openssl = { version = "0.10", features = ["vendored"] } prost = "0.12" rand = "0.8.5" diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 259f7919a3..74bee6d945 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -21,6 +21,7 @@ console_error_panic_hook = { version = "0.1" } futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } js-sys = { version = "0.3.61" } +json-proof-token = "0.3.4" proc_typescript = { version = "0.1.0", path = "./proc_typescript" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", default-features = false } @@ -29,11 +30,12 @@ serde_repr = { version = "0.1", default-features = false } tokio = { version = "1.29", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } +zkryptium = "0.2.2" [dependencies.identity_iota] path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021"] +features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"] [dev-dependencies] rand = "0.8.5" diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 2f50e4ed3d..db03dc07ec 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -17,6 +17,10 @@ if the object is being concurrently modified.

DIDUrl

A method agnostic DID Url.

+
DecodedJptCredential
+
+
DecodedJptPresentation
+
DecodedJws

A cryptographically verified decoded token from a JWS.

Contains the decoded headers and the raw claims.

@@ -67,11 +71,41 @@ if the object is being concurrently modified.

An extension interface that provides helper functions for publication and resolution of DID documents in Alias Outputs.

+
IssuerProtectedHeader
+
+
Jpt
+

A JSON Proof Token (JPT).

+
+
JptCredentialValidationOptions
+

Options to declare validation criteria for Jpt.

+
+
JptCredentialValidator
+
+
JptCredentialValidatorUtils
+

Utility functions for validating JPT credentials.

+
+
JptPresentationValidationOptions
+

Options to declare validation criteria for a Jpt presentation.

+
+
JptPresentationValidator
+
+
JptPresentationValidatorUtils
+

Utility functions for verifying JPT presentations.

+
Jwk
JwkGenOutput

The result of a key generation in JwkStorage.

+
JwpCredentialOptions
+
+
JwpIssued
+
+
JwpPresentationOptions
+

Options to be set in the JWT claims of a verifiable presentation.

+
+
JwpVerificationOptions
+
Jws

A wrapper around a JSON Web Signature (JWS).

@@ -123,8 +157,14 @@ use the methods pack and unpack instead.

MethodType

Supported verification method types.

+
PayloadEntry
+
+
Payloads
+
Presentation
+
PresentationProtectedHeader
+
Proof

Represents a cryptographic proof that can be used to validate verifiable credentials and presentations.

@@ -134,6 +174,8 @@ can be utilized to implement standards or user-defined proofs. The presence of t

Note that this proof is not related to JWT and can be used in combination or as an alternative to it.

+
ProofUpdateCtx
+
Resolver

Convenience type for resolving DID documents from different DID methods.

Also provides methods for resolving DID Documents associated with @@ -144,6 +186,9 @@ verifiable Credentials and Pre

RevocationBitmap

A compressed bitmap for managing credential revocation.

+
RevocationTimeframeStatus
+

Information used to determine the current status of a Credential.

+
SdJwt

Representation of an SD-JWT of the format <Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~<optional KB-JWT>.

@@ -159,6 +204,23 @@ verifiable Credentials and Pre with their corresponding disclosure digests.

Note: digests are created using the sha-256 algorithm.

+
SelectiveDisclosurePresentation
+

Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes

+
    +
  • @context MUST NOT be blinded
  • +
  • id MUST be blinded
  • +
  • type MUST NOT be blinded
  • +
  • issuer MUST NOT be blinded
  • +
  • issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
  • +
  • expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
  • +
  • credentialSubject (User have to choose which attribute must be blinded)
  • +
  • credentialSchema MUST NOT be blinded
  • +
  • credentialStatus MUST NOT be blinded
  • +
  • refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
  • +
  • termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
  • +
  • evidence (User have to choose which attribute must be blinded)
  • +
+
Service

A DID Document Service used to enable trusted interactions associated with a DID subject.

@@ -190,9 +252,31 @@ working with storage backed DID documents.

## Members
-
StatusPurpose
-

Purpose of a StatusList2021.

+
PresentationProofAlgorithm
+
+
ProofAlgorithm
+
+
StatusCheck
+

Controls validation behaviour when checking whether or not a credential has been revoked by its +credentialStatus.

+
+
Strict
+

Validate the status if supported, reject any unsupported +credentialStatus types.

+

Only RevocationBitmap2022 is currently supported.

+

This is the default.

+
+
SkipUnsupported
+

Validate the status if supported, skip any unsupported +credentialStatus types.

+
+
SkipAll
+

Skip all status checks.

+
SerializationType
+
+
MethodRelationship
+
SubjectHolderRelationship

Declares how credential subjects must relate to the presentation holder.

See also the Subject-Holder Relationship section of the specification.

@@ -207,6 +291,11 @@ This variant is the default.

Any

The holder is not required to have any kind of relationship to any credential subject.

+
CredentialStatus
+
+
StatusPurpose
+

Purpose of a StatusList2021.

+
StateMetadataEncoding
FailFast
@@ -218,6 +307,8 @@ This variant is the default.

FirstError

Return after the first error occurs.

+
PayloadType
+
MethodRelationship
CredentialStatus
@@ -252,6 +343,9 @@ This variant is the default.

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

+
start()
+

Initializes the console error panic hook for better error messages

+
encodeB64(data)string

Encode the given bytes in url-safe base64.

@@ -1335,6 +1429,74 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## DecodedJptCredential +**Kind**: global class + +* [DecodedJptCredential](#DecodedJptCredential) + * [.clone()](#DecodedJptCredential+clone) ⇒ [DecodedJptCredential](#DecodedJptCredential) + * [.credential()](#DecodedJptCredential+credential) ⇒ [Credential](#Credential) + * [.customClaims()](#DecodedJptCredential+customClaims) ⇒ Map.<string, any> + * [.decodedJwp()](#DecodedJptCredential+decodedJwp) ⇒ [JwpIssued](#JwpIssued) + + + +### decodedJptCredential.clone() ⇒ [DecodedJptCredential](#DecodedJptCredential) +Deep clones the object. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.credential() ⇒ [Credential](#Credential) +Returns the [Credential](#Credential) embedded into this JPT. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.customClaims() ⇒ Map.<string, any> +Returns the custom claims parsed from the JPT. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.decodedJwp() ⇒ [JwpIssued](#JwpIssued) +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +## DecodedJptPresentation +**Kind**: global class + +* [DecodedJptPresentation](#DecodedJptPresentation) + * [.clone()](#DecodedJptPresentation+clone) ⇒ [DecodedJptPresentation](#DecodedJptPresentation) + * [.credential()](#DecodedJptPresentation+credential) ⇒ [Credential](#Credential) + * [.customClaims()](#DecodedJptPresentation+customClaims) ⇒ Map.<string, any> + * [.aud()](#DecodedJptPresentation+aud) ⇒ string \| undefined + + + +### decodedJptPresentation.clone() ⇒ [DecodedJptPresentation](#DecodedJptPresentation) +Deep clones the object. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.credential() ⇒ [Credential](#Credential) +Returns the [Credential](#Credential) embedded into this JPT. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.customClaims() ⇒ Map.<string, any> +Returns the custom claims parsed from the JPT. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.aud() ⇒ string \| undefined +Returns the `aud` property parsed from the JWT claims. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) ## DecodedJws @@ -2058,6 +2220,11 @@ if the object is being concurrently modified. * [.createJws(storage, fragment, payload, options)](#IotaDocument+createJws) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options, [custom_claims])](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.generateMethodJwp(storage, alg, fragment, scope)](#IotaDocument+generateMethodJwp) ⇒ Promise.<string> + * [.createIssuedJwp(storage, fragment, jpt_claims, options)](#IotaDocument+createIssuedJwp) ⇒ Promise.<string> + * [.createPresentedJwp(presentation, method_id, options)](#IotaDocument+createPresentedJwp) ⇒ Promise.<string> + * [.createCredentialJpt(credential, storage, fragment, options, [custom_claims])](#IotaDocument+createCredentialJpt) ⇒ [Promise.<Jpt>](#Jpt) + * [.createPresentationJpt(presentation, method_id, options)](#IotaDocument+createPresentationJpt) ⇒ [Promise.<Jpt>](#Jpt) * _static_ * [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument](#IotaDocument) * [.unpackFromOutput(did, aliasOutput, allowEmpty)](#IotaDocument.unpackFromOutput) ⇒ [IotaDocument](#IotaDocument) @@ -2563,6 +2730,65 @@ private key backed by the `storage` in accordance with the passed `options`. | signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | | presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + + +### iotaDocument.generateMethodJwp(storage, alg, fragment, scope) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| alg | [ProofAlgorithm](#ProofAlgorithm) | +| fragment | string \| undefined | +| scope | [MethodScope](#MethodScope) | + + + +### iotaDocument.createIssuedJwp(storage, fragment, jpt_claims, options) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| jpt_claims | JptClaims | +| options | [JwpCredentialOptions](#JwpCredentialOptions) | + + + +### iotaDocument.createPresentedJwp(presentation, method_id, options) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| presentation | [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) | +| method_id | string | +| options | [JwpPresentationOptions](#JwpPresentationOptions) | + + + +### iotaDocument.createCredentialJpt(credential, storage, fragment, options, [custom_claims]) ⇒ [Promise.<Jpt>](#Jpt) +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| storage | [Storage](#Storage) | +| fragment | string | +| options | [JwpCredentialOptions](#JwpCredentialOptions) | +| [custom_claims] | Map.<string, any> \| undefined | + + + +### iotaDocument.createPresentationJpt(presentation, method_id, options) ⇒ [Promise.<Jpt>](#Jpt) +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| presentation | [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) | +| method_id | string | +| options | [JwpPresentationOptions](#JwpPresentationOptions) | + ### IotaDocument.newWithId(id) ⇒ [IotaDocument](#IotaDocument) @@ -2797,175 +3023,549 @@ Fetches the `IAliasOutput` associated with the given DID. | client | IIotaIdentityClient | | did | [IotaDID](#IotaDID) | - + -## Jwk +## IssuerProtectedHeader **Kind**: global class -* [Jwk](#Jwk) - * [new Jwk(jwk)](#new_Jwk_new) - * _instance_ - * [.kty()](#Jwk+kty) ⇒ JwkType - * [.use()](#Jwk+use) ⇒ JwkUse \| undefined - * [.keyOps()](#Jwk+keyOps) ⇒ Array.<JwkOperation> - * [.alg()](#Jwk+alg) ⇒ JwsAlgorithm \| undefined - * [.kid()](#Jwk+kid) ⇒ string \| undefined - * [.x5u()](#Jwk+x5u) ⇒ string \| undefined - * [.x5c()](#Jwk+x5c) ⇒ Array.<string> - * [.x5t()](#Jwk+x5t) ⇒ string \| undefined - * [.x5t256()](#Jwk+x5t256) ⇒ string \| undefined - * [.paramsEc()](#Jwk+paramsEc) ⇒ JwkParamsEc \| undefined - * [.paramsOkp()](#Jwk+paramsOkp) ⇒ JwkParamsOkp \| undefined - * [.paramsOct()](#Jwk+paramsOct) ⇒ JwkParamsOct \| undefined - * [.paramsRsa()](#Jwk+paramsRsa) ⇒ JwkParamsRsa \| undefined - * [.toPublic()](#Jwk+toPublic) ⇒ [Jwk](#Jwk) \| undefined - * [.isPublic()](#Jwk+isPublic) ⇒ boolean - * [.isPrivate()](#Jwk+isPrivate) ⇒ boolean - * [.toJSON()](#Jwk+toJSON) ⇒ any - * [.clone()](#Jwk+clone) ⇒ [Jwk](#Jwk) - * _static_ - * [.fromJSON(json)](#Jwk.fromJSON) ⇒ [Jwk](#Jwk) +* [IssuerProtectedHeader](#IssuerProtectedHeader) + * [.typ](#IssuerProtectedHeader+typ) ⇒ string \| undefined + * [.typ](#IssuerProtectedHeader+typ) + * [.alg](#IssuerProtectedHeader+alg) ⇒ [ProofAlgorithm](#ProofAlgorithm) + * [.alg](#IssuerProtectedHeader+alg) + * [.kid](#IssuerProtectedHeader+kid) ⇒ string \| undefined + * [.kid](#IssuerProtectedHeader+kid) + * [.cid](#IssuerProtectedHeader+cid) ⇒ string \| undefined + * [.cid](#IssuerProtectedHeader+cid) + * [.claims()](#IssuerProtectedHeader+claims) ⇒ Array.<string> - + -### new Jwk(jwk) +### issuerProtectedHeader.typ ⇒ string \| undefined +JWP type (JPT). + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + + +### issuerProtectedHeader.typ +JWP type (JPT). + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) | Param | Type | | --- | --- | -| jwk | IJwkParams | +| [arg0] | string \| undefined | - + -### jwk.kty() ⇒ JwkType -Returns the value for the key type parameter (kty). +### issuerProtectedHeader.alg ⇒ [ProofAlgorithm](#ProofAlgorithm) +Algorithm used for the JWP. -**Kind**: instance method of [Jwk](#Jwk) - +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + -### jwk.use() ⇒ JwkUse \| undefined -Returns the value for the use property (use). +### issuerProtectedHeader.alg +Algorithm used for the JWP. -**Kind**: instance method of [Jwk](#Jwk) - +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) -### jwk.keyOps() ⇒ Array.<JwkOperation> -**Kind**: instance method of [Jwk](#Jwk) - +| Param | Type | +| --- | --- | +| arg0 | [ProofAlgorithm](#ProofAlgorithm) | -### jwk.alg() ⇒ JwsAlgorithm \| undefined -Returns the value for the algorithm property (alg). + -**Kind**: instance method of [Jwk](#Jwk) - +### issuerProtectedHeader.kid ⇒ string \| undefined +ID for the key used for the JWP. -### jwk.kid() ⇒ string \| undefined -Returns the value of the key ID property (kid). +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + -**Kind**: instance method of [Jwk](#Jwk) - +### issuerProtectedHeader.kid +ID for the key used for the JWP. -### jwk.x5u() ⇒ string \| undefined -Returns the value of the X.509 URL property (x5u). +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) -**Kind**: instance method of [Jwk](#Jwk) - +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | -### jwk.x5c() ⇒ Array.<string> -Returns the value of the X.509 certificate chain property (x5c). + -**Kind**: instance method of [Jwk](#Jwk) - +### issuerProtectedHeader.cid ⇒ string \| undefined +Not handled for now. Will be used in the future to resolve external claims -### jwk.x5t() ⇒ string \| undefined -Returns the value of the X.509 certificate SHA-1 thumbprint property (x5t). +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + -**Kind**: instance method of [Jwk](#Jwk) - +### issuerProtectedHeader.cid +Not handled for now. Will be used in the future to resolve external claims -### jwk.x5t256() ⇒ string \| undefined -Returns the value of the X.509 certificate SHA-256 thumbprint property (x5t#S256). +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) -**Kind**: instance method of [Jwk](#Jwk) - +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | -### jwk.paramsEc() ⇒ JwkParamsEc \| undefined -If this JWK is of kty EC, returns those parameters. + -**Kind**: instance method of [Jwk](#Jwk) - +### issuerProtectedHeader.claims() ⇒ Array.<string> +**Kind**: instance method of [IssuerProtectedHeader](#IssuerProtectedHeader) + -### jwk.paramsOkp() ⇒ JwkParamsOkp \| undefined -If this JWK is of kty OKP, returns those parameters. +## Jpt +A JSON Proof Token (JPT). -**Kind**: instance method of [Jwk](#Jwk) - +**Kind**: global class -### jwk.paramsOct() ⇒ JwkParamsOct \| undefined -If this JWK is of kty OCT, returns those parameters. +* [Jpt](#Jpt) + * [new Jpt(jpt_string)](#new_Jpt_new) + * [.toString()](#Jpt+toString) ⇒ string + * [.clone()](#Jpt+clone) ⇒ [Jpt](#Jpt) -**Kind**: instance method of [Jwk](#Jwk) - + -### jwk.paramsRsa() ⇒ JwkParamsRsa \| undefined -If this JWK is of kty RSA, returns those parameters. +### new Jpt(jpt_string) +Creates a new [Jpt](#Jpt). -**Kind**: instance method of [Jwk](#Jwk) - -### jwk.toPublic() ⇒ [Jwk](#Jwk) \| undefined -Returns a clone of the [Jwk](#Jwk) with _all_ private key components unset. -Nothing is returned when `kty = oct` as this key type is not considered public by this library. +| Param | Type | +| --- | --- | +| jpt_string | string | -**Kind**: instance method of [Jwk](#Jwk) - + -### jwk.isPublic() ⇒ boolean -Returns `true` if _all_ private key components of the key are unset, `false` otherwise. +### jpt.toString() ⇒ string +**Kind**: instance method of [Jpt](#Jpt) + -**Kind**: instance method of [Jwk](#Jwk) - +### jpt.clone() ⇒ [Jpt](#Jpt) +Deep clones the object. -### jwk.isPrivate() ⇒ boolean -Returns `true` if _all_ private key components of the key are set, `false` otherwise. +**Kind**: instance method of [Jpt](#Jpt) + -**Kind**: instance method of [Jwk](#Jwk) - +## JptCredentialValidationOptions +Options to declare validation criteria for [Jpt](#Jpt). -### jwk.toJSON() ⇒ any -Serializes this to a JSON object. +**Kind**: global class -**Kind**: instance method of [Jwk](#Jwk) - +* [JptCredentialValidationOptions](#JptCredentialValidationOptions) + * [new JptCredentialValidationOptions([opts])](#new_JptCredentialValidationOptions_new) + * _instance_ + * [.clone()](#JptCredentialValidationOptions+clone) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) + * [.toJSON()](#JptCredentialValidationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JptCredentialValidationOptions.fromJSON) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) -### jwk.clone() ⇒ [Jwk](#Jwk) + + +### new JptCredentialValidationOptions([opts]) +Creates a new default istance. + + +| Param | Type | +| --- | --- | +| [opts] | IJptCredentialValidationOptions \| undefined | + + + +### jptCredentialValidationOptions.clone() ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) Deep clones the object. -**Kind**: instance method of [Jwk](#Jwk) - +**Kind**: instance method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) + -### Jwk.fromJSON(json) ⇒ [Jwk](#Jwk) +### jptCredentialValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) + + +### JptCredentialValidationOptions.fromJSON(json) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) Deserializes an instance from a JSON object. -**Kind**: static method of [Jwk](#Jwk) +**Kind**: static method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) | Param | Type | | --- | --- | | json | any | - - -## JwkGenOutput -The result of a key generation in `JwkStorage`. + +## JptCredentialValidator **Kind**: global class + -* [JwkGenOutput](#JwkGenOutput) - * [new JwkGenOutput(key_id, jwk)](#new_JwkGenOutput_new) - * _instance_ - * [.jwk()](#JwkGenOutput+jwk) ⇒ [Jwk](#Jwk) - * [.keyId()](#JwkGenOutput+keyId) ⇒ string - * [.toJSON()](#JwkGenOutput+toJSON) ⇒ any - * [.clone()](#JwkGenOutput+clone) ⇒ [JwkGenOutput](#JwkGenOutput) +### JptCredentialValidator.validate(credential_jpt, issuer, options, fail_fast) ⇒ [DecodedJptCredential](#DecodedJptCredential) +**Kind**: static method of [JptCredentialValidator](#JptCredentialValidator) + +| Param | Type | +| --- | --- | +| credential_jpt | [Jpt](#Jpt) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| options | [JptCredentialValidationOptions](#JptCredentialValidationOptions) | +| fail_fast | [FailFast](#FailFast) | + + + +## JptCredentialValidatorUtils +Utility functions for validating JPT credentials. + +**Kind**: global class + +* [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + * [.extractIssuer(credential)](#JptCredentialValidatorUtils.extractIssuer) ⇒ [CoreDID](#CoreDID) + * [.extractIssuerFromIssuedJpt(credential)](#JptCredentialValidatorUtils.extractIssuerFromIssuedJpt) ⇒ [CoreDID](#CoreDID) + * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024) + * [.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check)](#JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024) + * [.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024) + + + +### JptCredentialValidatorUtils.extractIssuer(credential) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a [`Credential`](`Credential`) as a DID. +# Errors +Fails if the issuer field is not a valid DID. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | + + + +### JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credential) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a credential in JPT representation as DID. +# Errors +If the JPT decoding fails or the issuer field is not a valid DID. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Jpt](#Jpt) | + + + +### JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check) +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + + + +### JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check) +Checks whether the credential status has been revoked. + +Only supports `RevocationTimeframe2024`. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| status_check | [StatusCheck](#StatusCheck) | + + + +### JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check) +Checks whether the credential status has been revoked or the timeframe interval is INVALID + +Only supports `RevocationTimeframe2024`. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + + + +## JptPresentationValidationOptions +Options to declare validation criteria for a [Jpt](#Jpt) presentation. + +**Kind**: global class + +* [JptPresentationValidationOptions](#JptPresentationValidationOptions) + * [new JptPresentationValidationOptions([opts])](#new_JptPresentationValidationOptions_new) + * _instance_ + * [.clone()](#JptPresentationValidationOptions+clone) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) + * [.toJSON()](#JptPresentationValidationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JptPresentationValidationOptions.fromJSON) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + + +### new JptPresentationValidationOptions([opts]) + +| Param | Type | +| --- | --- | +| [opts] | IJptPresentationValidationOptions \| undefined | + + + +### jptPresentationValidationOptions.clone() ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) +Deep clones the object. + +**Kind**: instance method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + +### jptPresentationValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + +### JptPresentationValidationOptions.fromJSON(json) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JptPresentationValidator +**Kind**: global class + + +### JptPresentationValidator.validate(presentation_jpt, issuer, options, fail_fast) ⇒ [DecodedJptPresentation](#DecodedJptPresentation) +Decodes and validates a Presented [Credential](#Credential) issued as a JPT (JWP Presented Form). A +[DecodedJptPresentation](#DecodedJptPresentation) is returned upon success. + +The following properties are validated according to `options`: +- the holder's proof on the JWP, +- the expiration date, +- the issuance date, +- the semantic structure. + +**Kind**: static method of [JptPresentationValidator](#JptPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation_jpt | [Jpt](#Jpt) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| options | [JptPresentationValidationOptions](#JptPresentationValidationOptions) | +| fail_fast | [FailFast](#FailFast) | + + + +## JptPresentationValidatorUtils +Utility functions for verifying JPT presentations. + +**Kind**: global class + +* [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + * [.extractIssuerFromPresentedJpt(presentation)](#JptPresentationValidatorUtils.extractIssuerFromPresentedJpt) ⇒ [CoreDID](#CoreDID) + * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024) + + + +### JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentation) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a credential in JPT representation as DID. +# Errors +If the JPT decoding fails or the issuer field is not a valid DID. + +**Kind**: static method of [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + +| Param | Type | +| --- | --- | +| presentation | [Jpt](#Jpt) | + + + +### JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check) +Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + +**Kind**: static method of [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + + + +## Jwk +**Kind**: global class + +* [Jwk](#Jwk) + * [new Jwk(jwk)](#new_Jwk_new) + * _instance_ + * [.kty()](#Jwk+kty) ⇒ JwkType + * [.use()](#Jwk+use) ⇒ JwkUse \| undefined + * [.keyOps()](#Jwk+keyOps) ⇒ Array.<JwkOperation> + * [.alg()](#Jwk+alg) ⇒ JwsAlgorithm \| undefined + * [.kid()](#Jwk+kid) ⇒ string \| undefined + * [.x5u()](#Jwk+x5u) ⇒ string \| undefined + * [.x5c()](#Jwk+x5c) ⇒ Array.<string> + * [.x5t()](#Jwk+x5t) ⇒ string \| undefined + * [.x5t256()](#Jwk+x5t256) ⇒ string \| undefined + * [.paramsEc()](#Jwk+paramsEc) ⇒ JwkParamsEc \| undefined + * [.paramsOkp()](#Jwk+paramsOkp) ⇒ JwkParamsOkp \| undefined + * [.paramsOct()](#Jwk+paramsOct) ⇒ JwkParamsOct \| undefined + * [.paramsRsa()](#Jwk+paramsRsa) ⇒ JwkParamsRsa \| undefined + * [.toPublic()](#Jwk+toPublic) ⇒ [Jwk](#Jwk) \| undefined + * [.isPublic()](#Jwk+isPublic) ⇒ boolean + * [.isPrivate()](#Jwk+isPrivate) ⇒ boolean + * [.toJSON()](#Jwk+toJSON) ⇒ any + * [.clone()](#Jwk+clone) ⇒ [Jwk](#Jwk) + * _static_ + * [.fromJSON(json)](#Jwk.fromJSON) ⇒ [Jwk](#Jwk) + + + +### new Jwk(jwk) + +| Param | Type | +| --- | --- | +| jwk | IJwkParams | + + + +### jwk.kty() ⇒ JwkType +Returns the value for the key type parameter (kty). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.use() ⇒ JwkUse \| undefined +Returns the value for the use property (use). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.keyOps() ⇒ Array.<JwkOperation> +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.alg() ⇒ JwsAlgorithm \| undefined +Returns the value for the algorithm property (alg). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.kid() ⇒ string \| undefined +Returns the value of the key ID property (kid). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.x5u() ⇒ string \| undefined +Returns the value of the X.509 URL property (x5u). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.x5c() ⇒ Array.<string> +Returns the value of the X.509 certificate chain property (x5c). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.x5t() ⇒ string \| undefined +Returns the value of the X.509 certificate SHA-1 thumbprint property (x5t). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.x5t256() ⇒ string \| undefined +Returns the value of the X.509 certificate SHA-256 thumbprint property (x5t#S256). + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.paramsEc() ⇒ JwkParamsEc \| undefined +If this JWK is of kty EC, returns those parameters. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.paramsOkp() ⇒ JwkParamsOkp \| undefined +If this JWK is of kty OKP, returns those parameters. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.paramsOct() ⇒ JwkParamsOct \| undefined +If this JWK is of kty OCT, returns those parameters. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.paramsRsa() ⇒ JwkParamsRsa \| undefined +If this JWK is of kty RSA, returns those parameters. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.toPublic() ⇒ [Jwk](#Jwk) \| undefined +Returns a clone of the [Jwk](#Jwk) with _all_ private key components unset. +Nothing is returned when `kty = oct` as this key type is not considered public by this library. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.isPublic() ⇒ boolean +Returns `true` if _all_ private key components of the key are unset, `false` otherwise. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.isPrivate() ⇒ boolean +Returns `true` if _all_ private key components of the key are set, `false` otherwise. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [Jwk](#Jwk) + + +### jwk.clone() ⇒ [Jwk](#Jwk) +Deep clones the object. + +**Kind**: instance method of [Jwk](#Jwk) + + +### Jwk.fromJSON(json) ⇒ [Jwk](#Jwk) +Deserializes an instance from a JSON object. + +**Kind**: static method of [Jwk](#Jwk) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwkGenOutput +The result of a key generation in `JwkStorage`. + +**Kind**: global class + +* [JwkGenOutput](#JwkGenOutput) + * [new JwkGenOutput(key_id, jwk)](#new_JwkGenOutput_new) + * _instance_ + * [.jwk()](#JwkGenOutput+jwk) ⇒ [Jwk](#Jwk) + * [.keyId()](#JwkGenOutput+keyId) ⇒ string + * [.toJSON()](#JwkGenOutput+toJSON) ⇒ any + * [.clone()](#JwkGenOutput+clone) ⇒ [JwkGenOutput](#JwkGenOutput) * _static_ * [.fromJSON(json)](#JwkGenOutput.fromJSON) ⇒ [JwkGenOutput](#JwkGenOutput) @@ -3013,6 +3613,217 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## JwpCredentialOptions +**Kind**: global class + +* [JwpCredentialOptions](#JwpCredentialOptions) + * _instance_ + * [.kid](#JwpCredentialOptions+kid) ⇒ string \| undefined + * [.kid](#JwpCredentialOptions+kid) + * [.toJSON()](#JwpCredentialOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(value)](#JwpCredentialOptions.fromJSON) ⇒ [JwpCredentialOptions](#JwpCredentialOptions) + + + +### jwpCredentialOptions.kid ⇒ string \| undefined +**Kind**: instance property of [JwpCredentialOptions](#JwpCredentialOptions) + + +### jwpCredentialOptions.kid +**Kind**: instance property of [JwpCredentialOptions](#JwpCredentialOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### jwpCredentialOptions.toJSON() ⇒ any +**Kind**: instance method of [JwpCredentialOptions](#JwpCredentialOptions) + + +### JwpCredentialOptions.fromJSON(value) ⇒ [JwpCredentialOptions](#JwpCredentialOptions) +**Kind**: static method of [JwpCredentialOptions](#JwpCredentialOptions) + +| Param | Type | +| --- | --- | +| value | any | + + + +## JwpIssued +**Kind**: global class + +* [JwpIssued](#JwpIssued) + * _instance_ + * [.toJSON()](#JwpIssued+toJSON) ⇒ any + * [.clone()](#JwpIssued+clone) ⇒ [JwpIssued](#JwpIssued) + * [.encode(serialization)](#JwpIssued+encode) ⇒ string + * [.setProof(proof)](#JwpIssued+setProof) + * [.getProof()](#JwpIssued+getProof) ⇒ Uint8Array + * [.getPayloads()](#JwpIssued+getPayloads) ⇒ [Payloads](#Payloads) + * [.setPayloads(payloads)](#JwpIssued+setPayloads) + * [.getIssuerProtectedHeader()](#JwpIssued+getIssuerProtectedHeader) ⇒ [IssuerProtectedHeader](#IssuerProtectedHeader) + * _static_ + * [.fromJSON(json)](#JwpIssued.fromJSON) ⇒ [JwpIssued](#JwpIssued) + + + +### jwpIssued.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.clone() ⇒ [JwpIssued](#JwpIssued) +Deep clones the object. + +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.encode(serialization) ⇒ string +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| serialization | [SerializationType](#SerializationType) | + + + +### jwpIssued.setProof(proof) +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| proof | Uint8Array | + + + +### jwpIssued.getProof() ⇒ Uint8Array +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.getPayloads() ⇒ [Payloads](#Payloads) +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.setPayloads(payloads) +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| payloads | [Payloads](#Payloads) | + + + +### jwpIssued.getIssuerProtectedHeader() ⇒ [IssuerProtectedHeader](#IssuerProtectedHeader) +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### JwpIssued.fromJSON(json) ⇒ [JwpIssued](#JwpIssued) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwpPresentationOptions +Options to be set in the JWT claims of a verifiable presentation. + +**Kind**: global class + +* [JwpPresentationOptions](#JwpPresentationOptions) + * [.audience](#JwpPresentationOptions+audience) ⇒ string \| undefined + * [.audience](#JwpPresentationOptions+audience) + * [.nonce](#JwpPresentationOptions+nonce) ⇒ string \| undefined + * [.nonce](#JwpPresentationOptions+nonce) + + + +### jwpPresentationOptions.audience ⇒ string \| undefined +Sets the audience for presentation (`aud` property in JWP Presentation Header). + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + + +### jwpPresentationOptions.audience +Sets the audience for presentation (`aud` property in JWP Presentation Header). + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### jwpPresentationOptions.nonce ⇒ string \| undefined +The nonce to be placed in the Presentation Protected Header. + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + + +### jwpPresentationOptions.nonce +The nonce to be placed in the Presentation Protected Header. + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +## JwpVerificationOptions +**Kind**: global class + +* [JwpVerificationOptions](#JwpVerificationOptions) + * _instance_ + * [.clone()](#JwpVerificationOptions+clone) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + * [.toJSON()](#JwpVerificationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JwpVerificationOptions.fromJSON) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + * [.new([opts])](#JwpVerificationOptions.new) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + + + +### jwpVerificationOptions.clone() ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +Deep clones the object. + +**Kind**: instance method of [JwpVerificationOptions](#JwpVerificationOptions) + + +### jwpVerificationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwpVerificationOptions](#JwpVerificationOptions) + + +### JwpVerificationOptions.fromJSON(json) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwpVerificationOptions](#JwpVerificationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +### JwpVerificationOptions.new([opts]) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +**Kind**: static method of [JwpVerificationOptions](#JwpVerificationOptions) + +| Param | Type | +| --- | --- | +| [opts] | IJwpVerificationOptions \| undefined | + ## Jws @@ -4681,22 +5492,159 @@ in the `publicKeyJwk` entry. ### MethodType.custom(type_) ⇒ [MethodType](#MethodType) A custom method. -**Kind**: static method of [MethodType](#MethodType) +**Kind**: static method of [MethodType](#MethodType) + +| Param | Type | +| --- | --- | +| type_ | string | + + + +### MethodType.fromJSON(json) ⇒ [MethodType](#MethodType) +Deserializes an instance from a JSON object. + +**Kind**: static method of [MethodType](#MethodType) + +| Param | Type | +| --- | --- | +| json | any | + + + +## PayloadEntry +**Kind**: global class + +* [PayloadEntry](#PayloadEntry) + * [.1](#PayloadEntry+1) ⇒ [PayloadType](#PayloadType) + * [.1](#PayloadEntry+1) + * [.value](#PayloadEntry+value) + * [.value](#PayloadEntry+value) ⇒ any + + + +### payloadEntry.1 ⇒ [PayloadType](#PayloadType) +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + + +### payloadEntry.1 +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + +| Param | Type | +| --- | --- | +| arg0 | [PayloadType](#PayloadType) | + + + +### payloadEntry.value +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + +| Param | Type | +| --- | --- | +| value | any | + + + +### payloadEntry.value ⇒ any +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + + +## Payloads +**Kind**: global class + +* [Payloads](#Payloads) + * [new Payloads(entries)](#new_Payloads_new) + * _instance_ + * [.toJSON()](#Payloads+toJSON) ⇒ any + * [.clone()](#Payloads+clone) ⇒ [Payloads](#Payloads) + * [.getValues()](#Payloads+getValues) ⇒ Array.<any> + * [.getUndisclosedIndexes()](#Payloads+getUndisclosedIndexes) ⇒ Uint32Array + * [.getDisclosedIndexes()](#Payloads+getDisclosedIndexes) ⇒ Uint32Array + * [.getUndisclosedPayloads()](#Payloads+getUndisclosedPayloads) ⇒ Array.<any> + * [.getDisclosedPayloads()](#Payloads+getDisclosedPayloads) ⇒ [Payloads](#Payloads) + * [.setUndisclosed(index)](#Payloads+setUndisclosed) + * [.replacePayloadAtIndex(index, value)](#Payloads+replacePayloadAtIndex) ⇒ any + * _static_ + * [.fromJSON(json)](#Payloads.fromJSON) ⇒ [Payloads](#Payloads) + * [.newFromValues(values)](#Payloads.newFromValues) ⇒ [Payloads](#Payloads) + + + +### new Payloads(entries) + +| Param | Type | +| --- | --- | +| entries | [Array.<PayloadEntry>](#PayloadEntry) | + + + +### payloads.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.clone() ⇒ [Payloads](#Payloads) +Deep clones the object. + +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getValues() ⇒ Array.<any> +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getUndisclosedIndexes() ⇒ Uint32Array +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getDisclosedIndexes() ⇒ Uint32Array +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getUndisclosedPayloads() ⇒ Array.<any> +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getDisclosedPayloads() ⇒ [Payloads](#Payloads) +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.setUndisclosed(index) +**Kind**: instance method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| index | number | + + + +### payloads.replacePayloadAtIndex(index, value) ⇒ any +**Kind**: instance method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| index | number | +| value | any | + + + +### Payloads.fromJSON(json) ⇒ [Payloads](#Payloads) +Deserializes an instance from a JSON object. + +**Kind**: static method of [Payloads](#Payloads) | Param | Type | | --- | --- | -| type_ | string | - - +| json | any | -### MethodType.fromJSON(json) ⇒ [MethodType](#MethodType) -Deserializes an instance from a JSON object. + -**Kind**: static method of [MethodType](#MethodType) +### Payloads.newFromValues(values) ⇒ [Payloads](#Payloads) +**Kind**: static method of [Payloads](#Payloads) | Param | Type | | --- | --- | -| json | any | +| values | Array.<any> | @@ -4835,6 +5783,85 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## PresentationProtectedHeader +**Kind**: global class + +* [PresentationProtectedHeader](#PresentationProtectedHeader) + * [.alg](#PresentationProtectedHeader+alg) ⇒ [PresentationProofAlgorithm](#PresentationProofAlgorithm) + * [.alg](#PresentationProtectedHeader+alg) + * [.kid](#PresentationProtectedHeader+kid) ⇒ string \| undefined + * [.kid](#PresentationProtectedHeader+kid) + * [.aud](#PresentationProtectedHeader+aud) ⇒ string \| undefined + * [.aud](#PresentationProtectedHeader+aud) + * [.nonce](#PresentationProtectedHeader+nonce) ⇒ string \| undefined + * [.nonce](#PresentationProtectedHeader+nonce) + + + +### presentationProtectedHeader.alg ⇒ [PresentationProofAlgorithm](#PresentationProofAlgorithm) +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.alg +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| arg0 | [PresentationProofAlgorithm](#PresentationProofAlgorithm) | + + + +### presentationProtectedHeader.kid ⇒ string \| undefined +ID for the key used for the JWP. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.kid +ID for the key used for the JWP. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### presentationProtectedHeader.aud ⇒ string \| undefined +Who have received the JPT. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.aud +Who have received the JPT. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### presentationProtectedHeader.nonce ⇒ string \| undefined +For replay attacks. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.nonce +For replay attacks. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + ## Proof @@ -4904,6 +5931,146 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## ProofUpdateCtx +**Kind**: global class + +* [ProofUpdateCtx](#ProofUpdateCtx) + * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe) ⇒ Uint8Array + * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe) + * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe) ⇒ Uint8Array + * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe) + * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe) ⇒ Uint8Array + * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe) + * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe) ⇒ Uint8Array + * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe) + * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe) ⇒ number + * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe) + * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe) ⇒ number + * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe) + * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages) ⇒ number + * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages) + + + +### proofUpdateCtx.old\_start\_validity\_timeframe ⇒ Uint8Array +Old `startValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.old\_start\_validity\_timeframe +Old `startValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.new\_start\_validity\_timeframe ⇒ Uint8Array +New `startValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.new\_start\_validity\_timeframe +New `startValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.old\_end\_validity\_timeframe ⇒ Uint8Array +Old `endValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.old\_end\_validity\_timeframe +Old `endValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.new\_end\_validity\_timeframe ⇒ Uint8Array +New `endValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.new\_end\_validity\_timeframe +New `endValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.index\_start\_validity\_timeframe ⇒ number +Index of `startValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.index\_start\_validity\_timeframe +Index of `startValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + + + +### proofUpdateCtx.index\_end\_validity\_timeframe ⇒ number +Index of `endValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.index\_end\_validity\_timeframe +Index of `endValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + + + +### proofUpdateCtx.number\_of\_signed\_messages ⇒ number +Number of signed messages, number of payloads in a JWP + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.number\_of\_signed\_messages +Number of signed messages, number of payloads in a JWP + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + ## Resolver @@ -5072,6 +6239,85 @@ if it is a valid Revocation Bitmap Service. | --- | --- | | service | [Service](#Service) | + + +## RevocationTimeframeStatus +Information used to determine the current status of a [Credential](#Credential). + +**Kind**: global class + +* [RevocationTimeframeStatus](#RevocationTimeframeStatus) + * [new RevocationTimeframeStatus(id, index, duration, [start_validity])](#new_RevocationTimeframeStatus_new) + * _instance_ + * [.clone()](#RevocationTimeframeStatus+clone) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) + * [.toJSON()](#RevocationTimeframeStatus+toJSON) ⇒ any + * [.startValidityTimeframe()](#RevocationTimeframeStatus+startValidityTimeframe) ⇒ [Timestamp](#Timestamp) + * [.endValidityTimeframe()](#RevocationTimeframeStatus+endValidityTimeframe) ⇒ [Timestamp](#Timestamp) + * [.id()](#RevocationTimeframeStatus+id) ⇒ string + * [.index()](#RevocationTimeframeStatus+index) ⇒ number \| undefined + * _static_ + * [.fromJSON(json)](#RevocationTimeframeStatus.fromJSON) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + + +### new RevocationTimeframeStatus(id, index, duration, [start_validity]) +Creates a new `RevocationTimeframeStatus`. + + +| Param | Type | +| --- | --- | +| id | string | +| index | number | +| duration | [Duration](#Duration) | +| [start_validity] | [Timestamp](#Timestamp) \| undefined | + + + +### revocationTimeframeStatus.clone() ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) +Deep clones the object. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.startValidityTimeframe() ⇒ [Timestamp](#Timestamp) +Get startValidityTimeframe value. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.endValidityTimeframe() ⇒ [Timestamp](#Timestamp) +Get endValidityTimeframe value. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.id() ⇒ string +Return the URL fo the `RevocationBitmapStatus`. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.index() ⇒ number \| undefined +Return the index of the credential in the issuer's revocation bitmap + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### RevocationTimeframeStatus.fromJSON(json) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) +Deserializes an instance from a JSON object. + +**Kind**: static method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + +| Param | Type | +| --- | --- | +| json | any | + ## SdJwt @@ -5409,6 +6655,93 @@ If path is an empty slice, decoys will be added to the top level. | path | string | | number_of_decoys | number | + + +## SelectiveDisclosurePresentation +Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes +- @context MUST NOT be blinded +- id MUST be blinded +- type MUST NOT be blinded +- issuer MUST NOT be blinded +- issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +- expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +- credentialSubject (User have to choose which attribute must be blinded) +- credentialSchema MUST NOT be blinded +- credentialStatus MUST NOT be blinded +- refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +- termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +- evidence (User have to choose which attribute must be blinded) + +**Kind**: global class + +* [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + * [new SelectiveDisclosurePresentation(issued_jwp)](#new_SelectiveDisclosurePresentation_new) + * [.concealInSubject(path)](#SelectiveDisclosurePresentation+concealInSubject) + * [.concealInEvidence(path)](#SelectiveDisclosurePresentation+concealInEvidence) + * [.setPresentationHeader(header)](#SelectiveDisclosurePresentation+setPresentationHeader) + + + +### new SelectiveDisclosurePresentation(issued_jwp) +Initialize a presentation starting from an Issued JWP. +The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. + + +| Param | Type | +| --- | --- | +| issued_jwp | [JwpIssued](#JwpIssued) | + + + +### selectiveDisclosurePresentation.concealInSubject(path) +Selectively disclose "credentialSubject" attributes. +# Example +``` +{ + "id": 1234, + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", +} +``` +If you want to undisclose for example the Mathematics course and the name of the degree: +``` +undisclose_subject("mainCourses[1]"); +undisclose_subject("degree.name"); +``` + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| path | string | + + + +### selectiveDisclosurePresentation.concealInEvidence(path) +Undiscloses "evidence" attributes. + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| path | string | + + + +### selectiveDisclosurePresentation.setPresentationHeader(header) +Sets presentation protected header. + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| header | [PresentationProtectedHeader](#PresentationProtectedHeader) | + ## Service @@ -6196,11 +7529,46 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - -## StatusPurpose -Purpose of a [StatusList2021](#StatusList2021). +**Kind**: global variable + + +## StatusCheck +Controls validation behaviour when checking whether or not a credential has been revoked by its +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + +**Kind**: global variable + + +## Strict +Validate the status if supported, reject any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. + +Only `RevocationBitmap2022` is currently supported. + +This is the default. + +**Kind**: global variable + + +## SkipUnsupported +Validate the status if supported, skip any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. + +**Kind**: global variable + + +## SkipAll +Skip all status checks. + +**Kind**: global variable + + +## SerializationType +**Kind**: global variable + +## MethodRelationship **Kind**: global variable @@ -6228,6 +7596,7 @@ The holder must match the subject only for credentials where the [`nonTransferab ## Any The holder is not required to have any kind of relationship to any credential subject. +## StateMetadataEncoding **Kind**: global variable @@ -6251,43 +7620,6 @@ Return all errors that occur during validation. Return after the first error occurs. **Kind**: global variable - - -## MethodRelationship -**Kind**: global variable - - -## CredentialStatus -**Kind**: global variable - - -## StatusCheck -Controls validation behaviour when checking whether or not a credential has been revoked by its -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). - -**Kind**: global variable - - -## Strict -Validate the status if supported, reject any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. - -Only `RevocationBitmap2022` is currently supported. - -This is the default. - -**Kind**: global variable - - -## SkipUnsupported -Validate the status if supported, skip any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. - -**Kind**: global variable - - -## SkipAll -Skip all status checks. **Kind**: global variable @@ -6312,6 +7644,12 @@ prior to calling the function. | decodedSignature | Uint8Array | | publicKey | [Jwk](#Jwk) | + + +## start() +Initializes the console error panic hook for better error messages + +**Kind**: global function ## encodeB64(data) ⇒ string diff --git a/bindings/wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/examples/src/1_advanced/8_zkp.ts new file mode 100644 index 0000000000..55d0c82fca --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/8_zkp.ts @@ -0,0 +1,226 @@ +import { + Credential, + FailFast, + IotaDID, + IotaDocument, + IotaIdentityClient, + JptCredentialValidationOptions, + JptCredentialValidator, + JptCredentialValidatorUtils, + JptPresentationValidationOptions, + JptPresentationValidator, + JptPresentationValidatorUtils, + JwkMemStore, + JwpCredentialOptions, + JwpPresentationOptions, + KeyIdMemStore, + MethodScope, + ProofAlgorithm, + SelectiveDisclosurePresentation, + Storage, +} from "@iota/identity-wasm/node"; +import { + type Address, + AliasOutput, + Client, + MnemonicSecretManager, + SecretManager, + SecretManagerType, + Utils, +} from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +/** Creates a DID Document and publishes it in a new Alias Output. + +Its functionality is equivalent to the "create DID" example +and exists for convenient calling from the other examples. */ +export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ + address: Address; + document: IotaDocument; + fragment: string; +}> { + const didClient = new IotaIdentityClient(client); + const networkHrp: string = await didClient.getNetworkHrp(); + + const secretManagerInstance = new SecretManager(secretManager); + const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ + accountIndex: 0, + range: { + start: 0, + end: 1, + }, + bech32Hrp: networkHrp, + }))[0]; + + console.log("Wallet address Bech32:", walletAddressBech32); + + await ensureAddressHasFunds(client, walletAddressBech32); + + const address: Address = Utils.parseBech32Address(walletAddressBech32); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + const document = new IotaDocument(networkHrp); + + const fragment = await document.generateMethodJwp( + storage, + ProofAlgorithm.BLS12381_SHA256, + undefined, + MethodScope.VerificationMethod(), + ); + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); + + // Publish the Alias Output and get the published DID document. + const published = await didClient.publishDidOutput(secretManager, aliasOutput); + + return { address, document: published, fragment }; +} +export async function zkp() { + // =========================================================================== + // Step 1: Create identity for the issuer. + // =========================================================================== + + // Create a new client to interact with the IOTA ledger. + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm. + // =========================================================================== + + // Create a credential subject indicating the degree earned by Alice. + const subject = { + name: "Alice", + mainCourses: ["Object-oriented Programming", "Mathematics"], + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + GPA: 4.0, + }; + + // Build credential using the above subject and issuer. + const credential = new Credential({ + id: "https:/example.edu/credentials/3732", + issuer: issuerDocument.id(), + type: "UniversityDegreeCredential", + credentialSubject: subject, + }); + const credentialJpt = await issuerDocument + .createCredentialJpt( + credential, + issuerStorage, + issuerFragment, + new JwpCredentialOptions(), + ); + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + const decodedJpt = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString()); + + // ============================================================================================ + // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential + // ============================================================================================ + const identityClient = new IotaIdentityClient(client); + + // Holder resolves issuer's DID. + let issuerDid = IotaDID.parse(JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credentialJpt).toString()); + let issuerDoc = await identityClient.resolveDid(issuerDid); + + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decodedCredential = JptCredentialValidator.validate( + credentialJpt, + issuerDoc, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Step 5: Verifier sends the holder a challenge and requests a Presentation. + // + // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations). + // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // ========================================================================================================= + // Step 6: Holder engages in the Selective Disclosure of credential's attributes. + // ========================================================================================================= + const methodId = decodedCredential + .decodedJwp() + .getIssuerProtectedHeader() + .kid!; + const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp()); + selectiveDisclosurePresentation.concealInSubject("mainCourses[1]"); + selectiveDisclosurePresentation.concealInSubject("degree.name"); + + // ======================================================================================================================================= + // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation + // JPT. + // ======================================================================================================================================= + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + const presentationOptions = new JwpPresentationOptions(); + presentationOptions.nonce = challenge; + const presentationJpt = await issuerDoc + .createPresentationJpt( + selectiveDisclosurePresentation, + methodId, + presentationOptions, + ); + + // =========================================================================== + // Step 8: Holder sends a Presentation JPT to the Verifier. + // =========================================================================== + + console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString()); + + // =========================================================================== + // Step 9: Verifier receives the Presentation and verifies it. + // =========================================================================== + + // Verifier resolve Issuer DID + const issuerDidV = IotaDID.parse( + JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentationJpt).toString(), + ); + const issuerDocV = await identityClient.resolveDid(issuerDidV); + + const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge }); + const decodedPresentedCredential = JptPresentationValidator.validate( + presentationJpt, + issuerDocV, + presentationValidationOptions, + FailFast.FirstError, + ); + + console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential()); +} diff --git a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts new file mode 100644 index 0000000000..e8c3d586a1 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts @@ -0,0 +1,281 @@ +import { + Credential, + Duration, + FailFast, + IotaDID, + IotaDocument, + IotaIdentityClient, + JptCredentialValidationOptions, + JptCredentialValidator, + JptCredentialValidatorUtils, + JptPresentationValidationOptions, + JptPresentationValidator, + JptPresentationValidatorUtils, + JwkMemStore, + JwpCredentialOptions, + JwpPresentationOptions, + KeyIdMemStore, + MethodScope, + ProofAlgorithm, + RevocationBitmap, + RevocationTimeframeStatus, + SelectiveDisclosurePresentation, + Status, + StatusCheck, + Storage, + Timestamp, +} from "@iota/identity-wasm/node"; +import { + type Address, + AliasOutput, + Client, + MnemonicSecretManager, + SecretManager, + SecretManagerType, + Utils, +} from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +/** Creates a DID Document and publishes it in a new Alias Output. + +Its functionality is equivalent to the "create DID" example +and exists for convenient calling from the other examples. */ +export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ + address: Address; + document: IotaDocument; + fragment: string; +}> { + const didClient = new IotaIdentityClient(client); + const networkHrp: string = await didClient.getNetworkHrp(); + + const secretManagerInstance = new SecretManager(secretManager); + const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ + accountIndex: 0, + range: { + start: 0, + end: 1, + }, + bech32Hrp: networkHrp, + }))[0]; + + console.log("Wallet address Bech32:", walletAddressBech32); + + await ensureAddressHasFunds(client, walletAddressBech32); + + const address: Address = Utils.parseBech32Address(walletAddressBech32); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + const document = new IotaDocument(networkHrp); + + const fragment = await document.generateMethodJwp( + storage, + ProofAlgorithm.BLS12381_SHA256, + undefined, + MethodScope.VerificationMethod(), + ); + const revocationBitmap = new RevocationBitmap(); + const serviceId = document.id().toUrl().join("#my-revocation-service"); + const service = revocationBitmap.toService(serviceId); + + document.insertService(service); + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); + + // Publish the Alias Output and get the published DID document. + const published = await didClient.publishDidOutput(secretManager, aliasOutput); + + return { address, document: published, fragment }; +} +export async function zkp_revocation() { + // Create a new client to interact with the IOTA ledger. + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + const holderSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const holderStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: holderDocument, fragment: holderFragment } = await createDid( + client, + holderSecretManager, + holderStorage, + ); + // ========================================================================================= + // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe + // ======================================================================================= + + const timeframeId = issuerDocument.id().toUrl().join("#my-revocation-service"); + let revocationTimeframeStatus = new RevocationTimeframeStatus( + timeframeId.toString(), + 5, + Duration.minutes(1), + Timestamp.nowUTC(), + ); + + // Create a credential subject indicating the degree earned by Alice. + const subject = { + name: "Alice", + mainCourses: ["Object-oriented Programming", "Mathematics"], + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + GPA: 4.0, + }; + + // Build credential using the above subject and issuer. + const credential = new Credential({ + id: "https:/example.edu/credentials/3732", + issuer: issuerDocument.id(), + type: "UniversityDegreeCredential", + credentialSubject: subject, + credentialStatus: revocationTimeframeStatus as any as Status, + }); + const credentialJpt = await issuerDocument + .createCredentialJpt( + credential, + issuerStorage, + issuerFragment, + new JwpCredentialOptions(), + ); + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + const decodedJpt = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString()); + + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decodedCredential = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Credential's Status check + // =========================================================================== + JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024( + decodedCredential.credential(), + issuerDocument, + undefined, + StatusCheck.Strict, + ); + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + const methodId = decodedCredential + .decodedJwp() + .getIssuerProtectedHeader() + .kid!; + + const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp()); + selectiveDisclosurePresentation.concealInSubject("mainCourses[1]"); + selectiveDisclosurePresentation.concealInSubject("degree.name"); + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + const presentationOptions = new JwpPresentationOptions(); + presentationOptions.nonce = challenge; + const presentationJpt = await issuerDocument + .createPresentationJpt( + selectiveDisclosurePresentation, + methodId, + presentationOptions, + ); + + console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString()); + + // =========================================================================== + // Step 2: Verifier receives the Presentation and verifies it. + // =========================================================================== + + const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge }); + const decodedPresentedCredential = JptPresentationValidator.validate( + presentationJpt, + issuerDocument, + presentationValidationOptions, + FailFast.FirstError, + ); + + JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024( + decodedPresentedCredential.credential(), + undefined, + StatusCheck.Strict, + ); + + console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential()); + + // =========================================================================== + // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid + // =========================================================================== + + try { + const now = new Date(); + const timeInTwoMinutes = new Date(now.setMinutes(now.getMinutes() + 2)); + JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024( + decodedPresentedCredential.credential(), + Timestamp.parse(timeInTwoMinutes.toISOString()), + StatusCheck.Strict, + ); + } catch (_) { + console.log("successfully expired!"); + } + + // =========================================================================== + // Issuer decides to Revoke Holder's Credential + // =========================================================================== + + console.log("Issuer decides to revoke the Credential"); + + const identityClient = new IotaIdentityClient(client); + + // Update the RevocationBitmap service in the issuer's DID Document. + // This revokes the credential's unique index. + issuerDocument.revokeCredentials("my-revocation-service", 5); + let aliasOutput = await identityClient.updateDidOutput(issuerDocument); + const rent = await identityClient.getRentStructure(); + aliasOutput = await client.buildAliasOutput({ + ...aliasOutput, + amount: Utils.computeStorageDeposit(aliasOutput, rent), + aliasId: aliasOutput.getAliasId(), + unlockConditions: aliasOutput.getUnlockConditions(), + }); + issuerDocument = await identityClient.publishDidOutput(issuerSecretManager, aliasOutput); + + // Holder checks if his credential has been revoked by the Issuer + try { + JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024( + decodedCredential.credential(), + issuerDocument, + StatusCheck.Strict, + ); + } catch (_) { + console.log("Credential revoked!"); + } +} diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index 145980e649..0a074d3fd2 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -17,6 +17,8 @@ import { customResolution } from "./1_advanced/4_custom_resolution"; import { domainLinkage } from "./1_advanced/5_domain_linkage"; import { sdJwt } from "./1_advanced/6_sd_jwt"; import { statusList2021 } from "./1_advanced/7_status_list_2021"; +import { zkp } from "./1_advanced/8_zkp"; +import { zkp_revocation } from "./1_advanced/9_zkp_revocation"; async function main() { // Extract example name. @@ -58,6 +60,10 @@ async function main() { return await sdJwt(); case "7_status_list_2021": return await statusList2021(); + case "8_zkp": + return await zkp(); + case "9_zkp_revocation": + return await zkp_revocation(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/examples/src/tests/8_zkp.ts b/bindings/wasm/examples/src/tests/8_zkp.ts new file mode 100644 index 0000000000..52d5b72bc4 --- /dev/null +++ b/bindings/wasm/examples/src/tests/8_zkp.ts @@ -0,0 +1,8 @@ +import { zkp } from "../1_advanced/8_zkp"; + +// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. +describe("Test node examples", function() { + it("zkp", async () => { + await zkp(); + }); +}); diff --git a/bindings/wasm/examples/src/tests/9_zkp_revocation.ts b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts new file mode 100644 index 0000000000..96075765f3 --- /dev/null +++ b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts @@ -0,0 +1,8 @@ +import { zkp_revocation } from "../1_advanced/9_zkp_revocation"; + +// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. +describe("Test node examples", function() { + it("zkp_revocation", async () => { + await zkp_revocation(); + }); +}); diff --git a/bindings/wasm/lib/jwk_storage.ts b/bindings/wasm/lib/jwk_storage.ts index 2c1156e5ac..235abcc8ce 100644 --- a/bindings/wasm/lib/jwk_storage.ts +++ b/bindings/wasm/lib/jwk_storage.ts @@ -1,5 +1,5 @@ import * as ed from "@noble/ed25519"; -import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage } from "~identity_wasm"; +import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage, ProofAlgorithm, ProofUpdateCtx } from "~identity_wasm"; import { EdCurve, JwkType, JwsAlgorithm } from "./jose"; type Ed25519PrivateKey = Uint8Array; @@ -18,6 +18,10 @@ export class JwkMemStore implements JwkStorage { return "Ed25519"; } + private _get_key(keyId: string): Jwk | undefined { + return this._keys.get(keyId); + } + public async generate(keyType: string, algorithm: JwsAlgorithm): Promise { if (keyType !== JwkMemStore.ed25519KeyType()) { throw new Error(`unsupported key type ${keyType}`); @@ -126,6 +130,23 @@ function decodeJwk(jwk: Jwk): [Ed25519PrivateKey, Ed25519PublicKey] { } } +export interface JwkStorageBBSPlusExt { + // Generate a new BLS12381 key represented as a JSON Web Key. + generateBBS: (algorithm: ProofAlgorithm) => Promise; + /** Signs a chunk of data together with an optional header + * using the private key corresponding to the given `keyId` and according + * to `publicKey`'s requirements. + */ + signBBS: (keyId: string, data: Uint8Array[], publicKey: Jwk, header?: Uint8Array) => Promise; + // Updates the timeframe validity period information of a given signature. + updateBBSSignature: ( + keyId: string, + publicKey: Jwk, + signature: Uint8Array, + proofCtx: ProofUpdateCtx, + ) => Promise; +} + // Returns a random number between `min` and `max` (inclusive). // SAFETY NOTE: This is not cryptographically secure randomness and thus not suitable for production use. // It suffices for our testing implementation however and avoids an external dependency. diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 193e761136..b9f3404439 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -15,7 +15,7 @@ "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", - "build:docs": "node ./build/docs", + "build:docs": "npm run fix_js_doc && node ./build/docs", "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve", "build": "npm run build:web && npm run build:nodejs && npm run build:docs", "example:node": "ts-node --project tsconfig.node.json -r tsconfig-paths/register ./examples/src/main.ts", @@ -28,7 +28,8 @@ "test:readme:rust": "mocha ./tests/txm_readme_rust.js --retries 3 --timeout 360000 --exit", "test:unit:node": "ts-mocha -p tsconfig.node.json ./tests/*.ts --parallel --exit", "cypress": "cypress open", - "fmt": "dprint fmt" + "fmt": "dprint fmt", + "fix_js_doc": "sed -Ei 's/\\((.*)\\)\\[\\]/\\1\\[\\]/' ./node/identity_wasm.js" }, "config": { "CYPRESS_VERIFY_TIMEOUT": 100000 diff --git a/bindings/wasm/src/common/types.rs b/bindings/wasm/src/common/types.rs index 295e0ea447..8264e923ce 100644 --- a/bindings/wasm/src/common/types.rs +++ b/bindings/wasm/src/common/types.rs @@ -75,3 +75,9 @@ impl TryFrom<&Object> for MapStringAny { Ok(map.unchecked_into::()) } } + +impl Default for MapStringAny { + fn default() -> Self { + js_sys::Map::new().unchecked_into() + } +} diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/src/credential/jpt.rs new file mode 100644 index 0000000000..e3e3daab2b --- /dev/null +++ b/bindings/wasm/src/credential/jpt.rs @@ -0,0 +1,45 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::Jpt; +use wasm_bindgen::prelude::*; + +/// A JSON Proof Token (JPT). +#[wasm_bindgen(js_name = Jpt)] +pub struct WasmJpt(pub(crate) Jpt); + +#[wasm_bindgen(js_class = Jpt)] +impl WasmJpt { + /// Creates a new {@link Jpt}. + #[wasm_bindgen(constructor)] + pub fn new(jpt_string: String) -> Self { + WasmJpt(Jpt::new(jpt_string)) + } + + // Returns the string representation for this {@link Jpt}. + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> String { + self.0.as_str().to_owned() + } +} + +impl_wasm_clone!(WasmJpt, Jpt); + +impl From for WasmJpt { + fn from(value: Jpt) -> Self { + WasmJpt(value) + } +} + +impl From for Jpt { + fn from(value: WasmJpt) -> Self { + value.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseJpt; +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs new file mode 100644 index 0000000000..46c999a40f --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs @@ -0,0 +1,52 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Object; +use identity_iota::credential::DecodedJptCredential; +use wasm_bindgen::prelude::*; + +use crate::common::MapStringAny; +use crate::credential::WasmCredential; +use crate::error::Result; +use crate::jpt::WasmJwpIssued; + +#[wasm_bindgen(js_name = DecodedJptCredential)] +pub struct WasmDecodedJptCredential(pub(crate) DecodedJptCredential); + +impl_wasm_clone!(WasmDecodedJptCredential, DecodedJptCredential); + +#[wasm_bindgen(js_class = DecodedJptCredential)] +impl WasmDecodedJptCredential { + /// Returns the {@link Credential} embedded into this JPT. + #[wasm_bindgen] + pub fn credential(&self) -> WasmCredential { + WasmCredential(self.0.credential.clone()) + } + + /// Returns the custom claims parsed from the JPT. + #[wasm_bindgen(js_name = "customClaims")] + pub fn custom_claims(&self) -> Result { + match self.0.custom_claims.clone() { + Some(obj) => MapStringAny::try_from(obj), + None => Ok(MapStringAny::default()), + } + } + + // The decoded and verified issued JWP, will be used to construct the presented JWP. + #[wasm_bindgen(js_name = decodedJwp)] + pub fn decoded_jwp(&self) -> WasmJwpIssued { + WasmJwpIssued(self.0.decoded_jwp.clone()) + } +} + +impl From for WasmDecodedJptCredential { + fn from(value: DecodedJptCredential) -> Self { + WasmDecodedJptCredential(value) + } +} + +impl From for DecodedJptCredential { + fn from(value: WasmDecodedJptCredential) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs new file mode 100644 index 0000000000..aefc6ec443 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs @@ -0,0 +1,80 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::JptCredentialValidationOptions; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; + +/// Options to declare validation criteria for {@link Jpt}. +#[derive(Debug, Default, Clone)] +#[wasm_bindgen(js_name = "JptCredentialValidationOptions", inspectable)] +pub struct WasmJptCredentialValidationOptions(pub(crate) JptCredentialValidationOptions); + +impl_wasm_clone!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions); +impl_wasm_json!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions); + +#[wasm_bindgen(js_class = JptCredentialValidationOptions)] +impl WasmJptCredentialValidationOptions { + /// Creates a new default istance. + #[wasm_bindgen(constructor)] + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts.into_serde().wasm_result().map(WasmJptCredentialValidationOptions) + } else { + Ok(WasmJptCredentialValidationOptions::default()) + } + } +} + +impl From for WasmJptCredentialValidationOptions { + fn from(value: JptCredentialValidationOptions) -> Self { + WasmJptCredentialValidationOptions(value) + } +} + +impl From for JptCredentialValidationOptions { + fn from(value: WasmJptCredentialValidationOptions) -> Self { + value.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJptCredentialValidationOptions")] + pub type IJptCredentialValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_CREDENTIAL_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JptCredentialValidationOptions}. */ +interface IJptCredentialValidationOptions { + /** + * Declare that the credential is **not** considered valid if it expires before this {@link Timestamp}. + * Uses the current datetime during validation if not set. + */ + readonly earliestExpiryDate?: Timestamp; + + /** + * Declare that the credential is **not** considered valid if it was issued later than this {@link Timestamp}. + * Uses the current datetime during validation if not set. + */ + readonly latestIssuanceDate?: Timestamp; + + /** + * Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + */ + readonly status?: StatusCheck; + + /** Declares how credential subjects must relate to the presentation holder during validation. + * + * + */ + readonly subjectHolderRelationship?: [string, SubjectHolderRelationship]; + + /** + * Options which affect the verification of the proof on the credential. + */ + readonly verificationOptions?: JwpVerificationOptions; +}"#; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs new file mode 100644 index 0000000000..10876fe96f --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::ImportedDocumentLock; +use crate::credential::WasmDecodedJptCredential; +use crate::credential::WasmFailFast; +use crate::credential::WasmJpt; +use crate::credential::WasmJptCredentialValidationOptions; +use crate::did::IToCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JptCredentialValidator; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JptCredentialValidator)] +pub struct WasmJptCredentialValidator; + +#[wasm_bindgen(js_class = JptCredentialValidator)] +impl WasmJptCredentialValidator { + #[wasm_bindgen] + pub fn validate( + credential_jpt: &WasmJpt, + issuer: &IToCoreDocument, + options: &WasmJptCredentialValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptCredentialValidator::validate(&credential_jpt.0, &issuer_guard, &options.0, fail_fast.into()) + .wasm_result() + .map(WasmDecodedJptCredential) + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs new file mode 100644 index 0000000000..cfdf9c6e9e --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs @@ -0,0 +1,102 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::ImportedDocumentLock; +use crate::common::WasmTimestamp; +use crate::credential::options::WasmStatusCheck; +use crate::credential::WasmCredential; +use crate::credential::WasmJpt; +use crate::did::IToCoreDocument; +use crate::did::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::core::Object; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::did::CoreDID; +use wasm_bindgen::prelude::*; + +/// Utility functions for validating JPT credentials. +#[wasm_bindgen(js_name = JptCredentialValidatorUtils)] +#[derive(Default)] +pub struct WasmJptCredentialValidatorUtils; + +#[wasm_bindgen(js_class = JptCredentialValidatorUtils)] +impl WasmJptCredentialValidatorUtils { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJptCredentialValidatorUtils { + WasmJptCredentialValidatorUtils + } + + /// Utility for extracting the issuer field of a {@link `Credential`} as a DID. + /// # Errors + /// Fails if the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuer")] + pub fn extract_issuer(credential: &WasmCredential) -> Result { + JptCredentialValidatorUtils::extract_issuer::(&credential.0) + .wasm_result() + .map(WasmCoreDID::from) + } + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// # Errors + /// If the JPT decoding fails or the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuerFromIssuedJpt")] + pub fn extract_issuer_from_issued_jpt(credential: &WasmJpt) -> Result { + JptCredentialValidatorUtils::extract_issuer_from_issued_jpt::(&credential.0) + .wasm_result() + .map(WasmCoreDID::from) + } + + #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")] + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &WasmCredential, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &credential.0, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } + + /// Checks whether the credential status has been revoked. + /// + /// Only supports `RevocationTimeframe2024`. + #[wasm_bindgen(js_name = "checkRevocationWithValidityTimeframe2024")] + pub fn check_revocation_with_validity_timeframe_2024( + credential: &WasmCredential, + issuer: &IToCoreDocument, + status_check: WasmStatusCheck, + ) -> Result<()> { + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &credential.0, + &issuer_guard, + status_check.into(), + ) + .wasm_result() + } + + /// Checks whether the credential status has been revoked or the timeframe interval is INVALID + /// + /// Only supports `RevocationTimeframe2024`. + #[wasm_bindgen(js_name = "checkTimeframesAndRevocationWithValidityTimeframe2024")] + pub fn check_timeframes_and_revocation_with_validity_timeframe_2024( + credential: &WasmCredential, + issuer: &IToCoreDocument, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024( + &credential.0, + &issuer_guard, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs new file mode 100644 index 0000000000..907e793996 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs @@ -0,0 +1,49 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JwpCredentialOptions; +use serde::Deserialize; +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwpCredentialOptions, getter_with_clone, inspectable)] +#[derive(Serialize, Deserialize, Default)] +pub struct WasmJwpCredentialOptions { + pub kid: Option, +} + +#[wasm_bindgen(js_class = JwpCredentialOptions)] +impl WasmJwpCredentialOptions { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJwpCredentialOptions { + WasmJwpCredentialOptions::default() + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> Result { + value.into_serde().wasm_result() + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + JsValue::from_serde(self).wasm_result() + } +} + +impl From for JwpCredentialOptions { + fn from(value: WasmJwpCredentialOptions) -> Self { + let WasmJwpCredentialOptions { kid } = value; + let mut jwp_options = JwpCredentialOptions::default(); + jwp_options.kid = kid; + + jwp_options + } +} + +impl From for WasmJwpCredentialOptions { + fn from(value: JwpCredentialOptions) -> Self { + WasmJwpCredentialOptions { kid: value.kid } + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs new file mode 100644 index 0000000000..d7ef8b5b89 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::document::verifiable::JwpVerificationOptions; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwpVerificationOptions, inspectable)] +#[derive(Clone, Debug, Default)] +pub struct WasmJwpVerificationOptions(pub(crate) JwpVerificationOptions); + +impl_wasm_clone!(WasmJwpVerificationOptions, JwpVerificationOptions); +impl_wasm_json!(WasmJwpVerificationOptions, JwpVerificationOptions); + +#[wasm_bindgen(js_class = JwpVerificationOptions)] +impl WasmJwpVerificationOptions { + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts.into_serde().wasm_result().map(WasmJwpVerificationOptions) + } else { + Ok(WasmJwpVerificationOptions::default()) + } + } +} + +// Interface to allow creating {@link JwpVerificationOptions} easily. +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwpVerificationOptions")] + pub type IJwpVerificationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JWP_VERIFICATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JwpVerificationOptions}. */ +interface IJwpVerificationOptions { + /** + * Verify the signing verification method relation matches this. + */ + readonly methodScope?: MethodScope; + + /** + * The DID URL of the method, whose JWK should be used to verify the JWP. + * If unset, the `kid` of the JWP is used as the DID URL. + */ + readonly methodId?: DIDUrl; +}"#; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs new file mode 100644 index 0000000000..7da2b15114 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_credential; +mod jpt_credential_validation_options; +mod jpt_credential_validator; +mod jpt_credential_validator_utils; +mod jwp_credential_options; +mod jwp_verification_options; + +pub use decoded_jpt_credential::*; +pub use jpt_credential_validation_options::*; +pub use jpt_credential_validator::*; +pub use jpt_credential_validator_utils::*; +pub use jwp_credential_options::*; +pub use jwp_verification_options::*; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs new file mode 100644 index 0000000000..698b9e3410 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs @@ -0,0 +1,51 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Object; +use identity_iota::credential::DecodedJptPresentation; +use wasm_bindgen::prelude::*; + +use crate::common::MapStringAny; +use crate::credential::WasmCredential; +use crate::error::Result; + +#[wasm_bindgen(js_name = DecodedJptPresentation)] +pub struct WasmDecodedJptPresentation(pub(crate) DecodedJptPresentation); + +impl_wasm_clone!(WasmDecodedJptPresentation, DecodedJptPresentation); + +#[wasm_bindgen(js_class = DecodedJptPresentation)] +impl WasmDecodedJptPresentation { + /// Returns the {@link Credential} embedded into this JPT. + #[wasm_bindgen] + pub fn credential(&self) -> WasmCredential { + WasmCredential(self.0.credential.clone()) + } + + /// Returns the custom claims parsed from the JPT. + #[wasm_bindgen(js_name = "customClaims")] + pub fn custom_claims(&self) -> Result { + match self.0.custom_claims.clone() { + Some(obj) => MapStringAny::try_from(obj), + None => Ok(MapStringAny::default()), + } + } + + /// Returns the `aud` property parsed from the JWT claims. + #[wasm_bindgen] + pub fn aud(&self) -> Option { + self.0.aud.as_ref().map(ToString::to_string) + } +} + +impl From for WasmDecodedJptPresentation { + fn from(value: DecodedJptPresentation) -> Self { + WasmDecodedJptPresentation(value) + } +} + +impl From for DecodedJptPresentation { + fn from(value: WasmDecodedJptPresentation) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs new file mode 100644 index 0000000000..2576437ed4 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs @@ -0,0 +1,64 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::JptPresentationValidationOptions; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; + +/// Options to declare validation criteria for a {@link Jpt} presentation. +#[derive(Debug, Default, Clone)] +#[wasm_bindgen(js_name = "JptPresentationValidationOptions", inspectable)] +pub struct WasmJptPresentationValidationOptions(pub(crate) JptPresentationValidationOptions); + +impl_wasm_clone!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions); +impl_wasm_json!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions); + +#[wasm_bindgen(js_class = JptPresentationValidationOptions)] +impl WasmJptPresentationValidationOptions { + #[wasm_bindgen(constructor)] + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts + .into_serde() + .wasm_result() + .map(WasmJptPresentationValidationOptions) + } else { + Ok(WasmJptPresentationValidationOptions::default()) + } + } +} + +impl From for WasmJptPresentationValidationOptions { + fn from(value: JptPresentationValidationOptions) -> Self { + WasmJptPresentationValidationOptions(value) + } +} + +impl From for JptPresentationValidationOptions { + fn from(value: WasmJptPresentationValidationOptions) -> Self { + value.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJptPresentationValidationOptions")] + pub type IJptPresentationValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JptPresentationValidationOptions}. */ +interface IJptPresentationValidationOptions { + /** + * The nonce to be placed in the Presentation Protected Header. + */ + readonly nonce?: string; + + /** + * Options which affect the verification of the proof on the credential. + */ + readonly verificationOptions?: JwpVerificationOptions; +}"#; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs new file mode 100644 index 0000000000..3843b48b81 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs @@ -0,0 +1,41 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::ImportedDocumentLock; +use crate::credential::WasmDecodedJptPresentation; +use crate::credential::WasmFailFast; +use crate::credential::WasmJpt; +use crate::credential::WasmJptPresentationValidationOptions; +use crate::did::IToCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JptPresentationValidator; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JptPresentationValidator)] +pub struct WasmJptPresentationValidator; + +#[wasm_bindgen(js_class = JptPresentationValidator)] +impl WasmJptPresentationValidator { + /// Decodes and validates a Presented {@link Credential} issued as a JPT (JWP Presented Form). A + /// {@link DecodedJptPresentation} is returned upon success. + /// + /// The following properties are validated according to `options`: + /// - the holder's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + #[wasm_bindgen] + pub fn validate( + presentation_jpt: &WasmJpt, + issuer: &IToCoreDocument, + options: &WasmJptPresentationValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptPresentationValidator::validate(&presentation_jpt.0, &issuer_guard, &options.0, fail_fast.into()) + .wasm_result() + .map(WasmDecodedJptPresentation) + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs new file mode 100644 index 0000000000..d3d927b82f --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs @@ -0,0 +1,44 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::WasmTimestamp; +use crate::credential::options::WasmStatusCheck; +use crate::credential::WasmCredential; +use crate::credential::WasmJpt; +use crate::did::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JptPresentationValidatorUtils; +use wasm_bindgen::prelude::*; + +/// Utility functions for verifying JPT presentations. +#[wasm_bindgen(js_name = JptPresentationValidatorUtils)] +pub struct WasmJptPresentationValidatorUtils; + +#[wasm_bindgen(js_class = JptPresentationValidatorUtils)] +impl WasmJptPresentationValidatorUtils { + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// # Errors + /// If the JPT decoding fails or the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuerFromPresentedJpt")] + pub fn extract_issuer_from_presented_jpt(presentation: &WasmJpt) -> Result { + JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation.0) + .wasm_result() + .map(WasmCoreDID) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")] + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &WasmCredential, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &credential.0, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs new file mode 100644 index 0000000000..7bc30851a5 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs @@ -0,0 +1,37 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Url; +use identity_iota::credential::JwpPresentationOptions; +use wasm_bindgen::prelude::*; + +/// Options to be set in the JWT claims of a verifiable presentation. +#[wasm_bindgen(js_name = JwpPresentationOptions, inspectable, getter_with_clone)] +#[derive(Default, Clone)] +pub struct WasmJwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWP Presentation Header). + pub audience: Option, + /// The nonce to be placed in the Presentation Protected Header. + pub nonce: Option, +} + +#[wasm_bindgen(js_class = JwpPresentationOptions)] +impl WasmJwpPresentationOptions { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJwpPresentationOptions { + Self::default() + } +} + +impl TryFrom for JwpPresentationOptions { + type Error = JsError; + fn try_from(value: WasmJwpPresentationOptions) -> Result { + let WasmJwpPresentationOptions { audience, nonce } = value; + let audience = audience + .map(Url::parse) + .transpose() + .map_err(|e| JsError::new(&e.to_string()))?; + + Ok(JwpPresentationOptions { audience, nonce }) + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs new file mode 100644 index 0000000000..8a2663c85f --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_presentation; +mod jpt_presentation_validation_options; +mod jpt_presentation_validator; +mod jpt_presentation_validator_utils; +mod jwp_presentation_options; + +pub use decoded_jpt_presentation::*; +pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator::*; +pub use jpt_presentation_validator_utils::*; +pub use jwp_presentation_options::*; diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 832eac1cd4..033a8cefd6 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -6,6 +6,9 @@ pub use self::credential::WasmCredential; pub use self::credential_builder::*; pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration; +pub use self::jpt::*; +pub use self::jpt_credential_validator::*; +pub use self::jpt_presentiation_validation::*; pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; @@ -22,6 +25,9 @@ mod credential_builder; mod domain_linkage_configuration; mod domain_linkage_credential_builder; mod domain_linkage_validator; +mod jpt; +mod jpt_credential_validator; +mod jpt_presentiation_validation; mod jws; mod jwt; mod jwt_credential_validation; diff --git a/bindings/wasm/src/credential/revocation/mod.rs b/bindings/wasm/src/credential/revocation/mod.rs index 7ad04980b4..c0f075df39 100644 --- a/bindings/wasm/src/credential/revocation/mod.rs +++ b/bindings/wasm/src/credential/revocation/mod.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod status_list_2021; +pub mod validity_timeframe_2024; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs new file mode 100644 index 0000000000..36474c70bb --- /dev/null +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod status; + +pub use status::*; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs new file mode 100644 index 0000000000..fb85bbeee3 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs @@ -0,0 +1,75 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Url; +use identity_iota::credential::RevocationTimeframeStatus; +use wasm_bindgen::prelude::*; + +use crate::common::WasmDuration; +use crate::common::WasmTimestamp; +use crate::error::Result; +use crate::error::WasmResult; + +/// Information used to determine the current status of a {@link Credential}. +#[wasm_bindgen(js_name = RevocationTimeframeStatus, inspectable)] +pub struct WasmRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus); + +impl_wasm_clone!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus); +impl_wasm_json!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus); + +#[wasm_bindgen(js_class = RevocationTimeframeStatus)] +impl WasmRevocationTimeframeStatus { + /// Creates a new `RevocationTimeframeStatus`. + #[wasm_bindgen(constructor)] + pub fn new( + id: String, + index: u32, + duration: WasmDuration, + start_validity: Option, + ) -> Result { + RevocationTimeframeStatus::new( + start_validity.map(|t| t.0), + duration.0, + Url::parse(id).wasm_result()?, + index, + ) + .wasm_result() + .map(WasmRevocationTimeframeStatus) + } + + /// Get startValidityTimeframe value. + #[wasm_bindgen(js_name = "startValidityTimeframe")] + pub fn start_validity_timeframe(&self) -> WasmTimestamp { + self.0.start_validity_timeframe().into() + } + + /// Get endValidityTimeframe value. + #[wasm_bindgen(js_name = "endValidityTimeframe")] + pub fn end_validity_timeframe(&self) -> WasmTimestamp { + self.0.end_validity_timeframe().into() + } + + /// Return the URL fo the `RevocationBitmapStatus`. + #[wasm_bindgen] + pub fn id(&self) -> String { + self.0.id().to_string() + } + + /// Return the index of the credential in the issuer's revocation bitmap + #[wasm_bindgen] + pub fn index(&self) -> Option { + self.0.index() + } +} + +impl From for WasmRevocationTimeframeStatus { + fn from(value: RevocationTimeframeStatus) -> Self { + WasmRevocationTimeframeStatus(value) + } +} + +impl From for RevocationTimeframeStatus { + fn from(value: WasmRevocationTimeframeStatus) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 0bae16c048..0fe08e6675 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -835,6 +835,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "Promise")] pub type PromiseJwt; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseJpt; } #[wasm_bindgen(typescript_custom_section)] diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index d7e8dfa3d8..035e7838bf 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -126,6 +126,8 @@ macro_rules! impl_wasm_error_from_with_struct_name { } } +impl_wasm_error_from_with_struct_name!(jsonprooftoken::errors::CustomError); + // identity_iota::iota now has some errors where the error message does not include the source error's error message. // This is in compliance with the Rust error handling project group's recommendation: // * An error type with a source error should either return that error via source or include that source's error message diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 8d004422ad..777a00e679 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -44,13 +44,16 @@ use crate::common::RecordStringAny; use crate::common::UDIDUrlQuery; use crate::common::UOneOrManyNumber; use crate::common::WasmTimestamp; +use crate::credential::PromiseJpt; use crate::credential::UnknownCredential; use crate::credential::WasmCredential; +use crate::credential::WasmJpt; +use crate::credential::WasmJwpCredentialOptions; +use crate::credential::WasmJwpPresentationOptions; use crate::credential::WasmJws; use crate::credential::WasmJwt; use crate::credential::WasmPresentation; use crate::did::CoreDocumentLock; - use crate::did::PromiseJws; use crate::did::PromiseJwt; use crate::did::WasmCoreDocument; @@ -65,6 +68,9 @@ use crate::iota::WasmIotaDocumentMetadata; use crate::iota::WasmStateMetadataEncoding; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; +use crate::jpt::WasmJptClaims; +use crate::jpt::WasmProofAlgorithm; +use crate::jpt::WasmSelectiveDisclosurePresentation; use crate::storage::WasmJwsSignatureOptions; use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; @@ -75,6 +81,7 @@ use crate::verification::WasmJwsVerifier; use crate::verification::WasmMethodRelationship; use crate::verification::WasmMethodScope; use crate::verification::WasmVerificationMethod; +use identity_iota::storage::JwpDocumentExt; pub(crate) struct IotaDocumentLock(tokio::sync::RwLock); @@ -852,6 +859,140 @@ impl WasmIotaDocument { }); Ok(promise.unchecked_into()) } + + #[wasm_bindgen(js_name = generateMethodJwp)] + pub fn generate_method_jwp( + &self, + storage: &WasmStorage, + alg: WasmProofAlgorithm, + fragment: Option, + scope: WasmMethodScope, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let promise: Promise = future_to_promise(async move { + let method_fragment: String = document_lock_clone + .write() + .await + .generate_method_jwp( + &storage_clone, + KeyType::from_static_str("BLS12381"), + alg.into(), + fragment.as_deref(), + scope.0, + ) + .await + .wasm_result()?; + Ok(JsValue::from(method_fragment)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createIssuedJwp)] + pub fn create_issued_jwp( + &self, + storage: &WasmStorage, + fragment: String, + jpt_claims: WasmJptClaims, + options: WasmJwpCredentialOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let jpt_claims = jpt_claims.into_serde().wasm_result()?; + let storage_clone: Rc = storage.0.clone(); + let options = options.into(); + let promise: Promise = future_to_promise(async move { + let jwp: String = document_lock_clone + .write() + .await + .create_issued_jwp(&storage_clone, fragment.as_str(), &jpt_claims, &options) + .await + .wasm_result()?; + Ok(JsValue::from(jwp)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createPresentedJwp)] + pub fn create_presented_jwp( + &self, + presentation: WasmSelectiveDisclosurePresentation, + method_id: String, + options: WasmJwpPresentationOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let options = options.try_into()?; + let promise: Promise = future_to_promise(async move { + let mut presentation = presentation.0; + let jwp: String = document_lock_clone + .write() + .await + .create_presented_jwp(&mut presentation, method_id.as_str(), &options) + .await + .wasm_result()?; + Ok(JsValue::from(jwp)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createCredentialJpt)] + pub fn create_credential_jpt( + &self, + credential: WasmCredential, + storage: &WasmStorage, + fragment: String, + options: WasmJwpCredentialOptions, + custom_claims: Option, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let options = options.into(); + let custom_claims = custom_claims.and_then(|claims| claims.into_serde().ok()); + let promise: Promise = future_to_promise(async move { + let jpt = document_lock_clone + .write() + .await + .create_credential_jpt( + &credential.0, + &storage_clone, + fragment.as_str(), + &options, + custom_claims, + ) + .await + .map(WasmJpt) + .wasm_result()?; + Ok(JsValue::from(jpt)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createPresentationJpt)] + pub fn create_presentation_jpt( + &self, + presentation: WasmSelectiveDisclosurePresentation, + method_id: String, + options: WasmJwpPresentationOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let options = options.try_into()?; + let promise: Promise = future_to_promise(async move { + let mut presentation = presentation.0; + let jpt = document_lock_clone + .write() + .await + .create_presentation_jpt(&mut presentation, method_id.as_str(), &options) + .await + .map(WasmJpt) + .wasm_result()?; + Ok(JsValue::from(jpt)) + }); + + Ok(promise.unchecked_into()) + } } impl From for WasmIotaDocument { diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/src/jpt/encoding.rs new file mode 100644 index 0000000000..e36a5307a5 --- /dev/null +++ b/bindings/wasm/src/jpt/encoding.rs @@ -0,0 +1,29 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use jsonprooftoken::encoding::SerializationType; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = SerializationType)] +pub enum WasmSerializationType { + COMPACT = 0, + JSON = 1, +} + +impl From for SerializationType { + fn from(value: WasmSerializationType) -> Self { + match value { + WasmSerializationType::COMPACT => SerializationType::COMPACT, + WasmSerializationType::JSON => SerializationType::JSON, + } + } +} + +impl From for WasmSerializationType { + fn from(value: SerializationType) -> Self { + match value { + SerializationType::COMPACT => WasmSerializationType::COMPACT, + SerializationType::JSON => WasmSerializationType::JSON, + } + } +} diff --git a/bindings/wasm/src/jpt/issuer_protected_header.rs b/bindings/wasm/src/jpt/issuer_protected_header.rs new file mode 100644 index 0000000000..4499d42b69 --- /dev/null +++ b/bindings/wasm/src/jpt/issuer_protected_header.rs @@ -0,0 +1,52 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::jpt::WasmProofAlgorithm; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = IssuerProtectedHeader, getter_with_clone, inspectable)] +pub struct WasmIssuerProtectedHeader { + /// JWP type (JPT). + pub typ: Option, + /// Algorithm used for the JWP. + pub alg: WasmProofAlgorithm, + /// ID for the key used for the JWP. + pub kid: Option, + /// Not handled for now. Will be used in the future to resolve external claims + pub cid: Option, + /// Claims. + claims: Vec, +} + +#[wasm_bindgen(js_class = IssuerProtectedHeader)] +impl WasmIssuerProtectedHeader { + #[wasm_bindgen] + pub fn claims(&self) -> Vec { + self.claims.clone() + } +} + +impl From for IssuerProtectedHeader { + fn from(value: WasmIssuerProtectedHeader) -> Self { + let WasmIssuerProtectedHeader { typ, alg, kid, cid, .. } = value; + let mut header = IssuerProtectedHeader::new(alg.into()); + header.set_typ(typ); + header.set_kid(kid); + header.set_cid(cid); + + header + } +} + +impl From for WasmIssuerProtectedHeader { + fn from(value: IssuerProtectedHeader) -> Self { + WasmIssuerProtectedHeader { + typ: value.typ().cloned(), + alg: value.alg().into(), + kid: value.kid().cloned(), + cid: value.cid().cloned(), + claims: value.claims().map(|claims| claims.clone().0).unwrap_or_default(), + } + } +} diff --git a/bindings/wasm/src/jpt/jpt_claims.rs b/bindings/wasm/src/jpt/jpt_claims.rs new file mode 100644 index 0000000000..ae9a6e0822 --- /dev/null +++ b/bindings/wasm/src/jpt/jpt_claims.rs @@ -0,0 +1,31 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "JptClaims")] + pub type WasmJptClaims; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_CLAIMS: &'static str = r#" +/** JPT claims */ + +interface JptClaims { + /** Who issued the JWP*/ + readonly iss?: string; + /** Subject of the JPT. */ + readonly sub?: string; + /** Expiration time. */ + readonly exp?: number; + /** Issuance date. */ + readonly iat?: number; + /** Time before which the JPT MUST NOT be accepted */ + readonly nbf?: number; + /** Unique ID for the JPT. */ + readonly jti?: string; + /** Custom claims. */ + readonly [properties: string]: any; +}"#; diff --git a/bindings/wasm/src/jpt/jwp_issued.rs b/bindings/wasm/src/jpt/jwp_issued.rs new file mode 100644 index 0000000000..e2c3826621 --- /dev/null +++ b/bindings/wasm/src/jpt/jwp_issued.rs @@ -0,0 +1,50 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use jsonprooftoken::jwp::issued::JwpIssued; +use wasm_bindgen::prelude::*; + +use super::WasmPayloads; +use super::WasmSerializationType; +use crate::error::Result; +use crate::error::WasmResult; +use crate::jpt::WasmIssuerProtectedHeader; + +#[wasm_bindgen(js_name = JwpIssued)] +pub struct WasmJwpIssued(pub(crate) JwpIssued); + +impl_wasm_json!(WasmJwpIssued, JwpIssued); +impl_wasm_clone!(WasmJwpIssued, JwpIssued); + +#[wasm_bindgen(js_class = JwpIssued)] +impl WasmJwpIssued { + #[wasm_bindgen] + pub fn encode(&self, serialization: WasmSerializationType) -> Result { + self.0.encode(serialization.into()).wasm_result() + } + + #[wasm_bindgen(js_name = "setProof")] + pub fn set_proof(&mut self, proof: &[u8]) { + self.0.set_proof(proof) + } + + #[wasm_bindgen(js_name = "getProof")] + pub fn get_proof(&self) -> Vec { + self.0.get_proof().to_owned() + } + + #[wasm_bindgen(js_name = "getPayloads")] + pub fn get_payloads(&self) -> WasmPayloads { + self.0.get_payloads().clone().into() + } + + #[wasm_bindgen(js_name = "setPayloads")] + pub fn set_payloads(&mut self, payloads: WasmPayloads) { + self.0.set_payloads(payloads.into()) + } + + #[wasm_bindgen(js_name = getIssuerProtectedHeader)] + pub fn get_issuer_protected_header(&self) -> WasmIssuerProtectedHeader { + self.0.get_issuer_protected_header().clone().into() + } +} diff --git a/bindings/wasm/src/jpt/jwp_presentation_builder.rs b/bindings/wasm/src/jpt/jwp_presentation_builder.rs new file mode 100644 index 0000000000..64797ee4cf --- /dev/null +++ b/bindings/wasm/src/jpt/jwp_presentation_builder.rs @@ -0,0 +1,83 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::WasmJwpIssued; +use super::WasmPresentationProtectedHeader; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::SelectiveDisclosurePresentation; +use wasm_bindgen::prelude::*; + +/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes +/// - @context MUST NOT be blinded +/// - id MUST be blinded +/// - type MUST NOT be blinded +/// - issuer MUST NOT be blinded +/// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - credentialSubject (User have to choose which attribute must be blinded) +/// - credentialSchema MUST NOT be blinded +/// - credentialStatus MUST NOT be blinded +/// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +/// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +/// - evidence (User have to choose which attribute must be blinded) +#[wasm_bindgen(js_name = SelectiveDisclosurePresentation)] +pub struct WasmSelectiveDisclosurePresentation(pub(crate) SelectiveDisclosurePresentation); + +impl From for SelectiveDisclosurePresentation { + fn from(value: WasmSelectiveDisclosurePresentation) -> Self { + value.0 + } +} + +impl From for WasmSelectiveDisclosurePresentation { + fn from(value: SelectiveDisclosurePresentation) -> Self { + WasmSelectiveDisclosurePresentation(value) + } +} + +#[wasm_bindgen(js_class = SelectiveDisclosurePresentation)] +impl WasmSelectiveDisclosurePresentation { + /// Initialize a presentation starting from an Issued JWP. + /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. + #[wasm_bindgen(constructor)] + pub fn new(issued_jwp: WasmJwpIssued) -> WasmSelectiveDisclosurePresentation { + SelectiveDisclosurePresentation::new(&issued_jwp.0).into() + } + + /// Selectively disclose "credentialSubject" attributes. + /// # Example + /// ``` + /// { + /// "id": 1234, + /// "name": "Alice", + /// "mainCourses": ["Object-oriented Programming", "Mathematics"], + /// "degree": { + /// "type": "BachelorDegree", + /// "name": "Bachelor of Science and Arts", + /// }, + /// "GPA": "4.0", + /// } + /// ``` + /// If you want to undisclose for example the Mathematics course and the name of the degree: + /// ``` + /// undisclose_subject("mainCourses[1]"); + /// undisclose_subject("degree.name"); + /// ``` + #[wasm_bindgen(js_name = concealInSubject)] + pub fn conceal_in_subject(&mut self, path: String) -> Result<()> { + self.0.conceal_in_subject(&path).wasm_result() + } + + /// Undiscloses "evidence" attributes. + #[wasm_bindgen(js_name = concealInEvidence)] + pub fn conceal_in_evidence(&mut self, path: String) -> Result<()> { + self.0.conceal_in_evidence(&path).wasm_result() + } + + /// Sets presentation protected header. + #[wasm_bindgen(js_name = setPresentationHeader)] + pub fn set_presentation_header(&mut self, header: WasmPresentationProtectedHeader) { + self.0.set_presentation_header(header.into()) + } +} diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs new file mode 100644 index 0000000000..631572003b --- /dev/null +++ b/bindings/wasm/src/jpt/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod encoding; +mod issuer_protected_header; +mod jpt_claims; +mod jwp_issued; +mod jwp_presentation_builder; +mod payload; +mod presentation_protected_header; +mod proof_algorithm; + +pub use encoding::*; +pub use issuer_protected_header::*; +pub use jpt_claims::*; +pub use jwp_issued::*; +pub use jwp_presentation_builder::*; +pub use payload::*; +pub use presentation_protected_header::*; +pub use proof_algorithm::*; diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/src/jpt/payload.rs new file mode 100644 index 0000000000..cdb06638b3 --- /dev/null +++ b/bindings/wasm/src/jpt/payload.rs @@ -0,0 +1,151 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmError; +use crate::error::WasmResult; +use jsonprooftoken::jpt::payloads::PayloadType; +use jsonprooftoken::jpt::payloads::Payloads; +use serde_json::Value; +use std::borrow::Cow; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[wasm_bindgen(js_name = PayloadType)] +#[derive(Clone, Copy, Debug)] +pub enum WasmPayloadType { + Disclosed = 0, + Undisclosed = 1, + ProofMethods = 2, +} + +impl From for PayloadType { + fn from(value: WasmPayloadType) -> PayloadType { + match value { + WasmPayloadType::Disclosed => PayloadType::Disclosed, + WasmPayloadType::ProofMethods => PayloadType::ProofMethods, + WasmPayloadType::Undisclosed => PayloadType::Undisclosed, + } + } +} + +impl From for WasmPayloadType { + fn from(value: PayloadType) -> WasmPayloadType { + match value { + PayloadType::Disclosed => WasmPayloadType::Disclosed, + PayloadType::ProofMethods => WasmPayloadType::ProofMethods, + PayloadType::Undisclosed => WasmPayloadType::Undisclosed, + } + } +} + +#[wasm_bindgen(js_name = PayloadEntry)] +pub struct WasmPayloadEntry(JsValue, pub WasmPayloadType); + +#[wasm_bindgen(js_class = PayloadEntry)] +impl WasmPayloadEntry { + #[wasm_bindgen(setter)] + pub fn set_value(&mut self, value: JsValue) { + self.0 = value; + } + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + self.0.clone() + } +} + +#[wasm_bindgen(js_name = Payloads, inspectable)] +pub struct WasmPayloads(pub(crate) Payloads); + +impl_wasm_json!(WasmPayloads, Payloads); +impl_wasm_clone!(WasmPayloads, Payloads); + +#[wasm_bindgen(js_class = Payloads)] +impl WasmPayloads { + #[wasm_bindgen(constructor)] + pub fn new(entries: Vec) -> Result { + entries + .into_iter() + .map(|WasmPayloadEntry(value, type_)| value.into_serde().wasm_result().map(|value| (value, type_.into()))) + .collect::>>() + .map(Payloads) + .map(WasmPayloads) + } + + #[wasm_bindgen(js_name = newFromValues)] + pub fn new_from_values(values: Vec) -> Result { + let values = values + .into_iter() + .map(|v| v.into_serde().wasm_result()) + .collect::>>()?; + + Ok(Payloads::new_from_values(values).into()) + } + + #[wasm_bindgen(js_name = "getValues")] + pub fn get_values(&self) -> Result> { + self + .0 + .get_values() + .into_iter() + .map(|value| JsValue::from_serde(&value).wasm_result()) + .collect() + } + + #[wasm_bindgen(js_name = "getUndisclosedIndexes")] + pub fn get_undisclosed_indexes(&self) -> Vec { + self.0.get_undisclosed_indexes() + } + + #[wasm_bindgen(js_name = "getDisclosedIndexes")] + pub fn get_disclosed_indexes(&self) -> Vec { + self.0.get_disclosed_indexes() + } + + #[wasm_bindgen(js_name = "getUndisclosedPayloads")] + pub fn get_undisclosed_payloads(&self) -> Result> { + self + .0 + .get_undisclosed_payloads() + .into_iter() + .map(|value| JsValue::from_serde(&value).wasm_result()) + .collect() + } + + #[wasm_bindgen(js_name = "getDisclosedPayloads")] + pub fn get_disclosed_payloads(&self) -> WasmPayloads { + self.0.get_disclosed_payloads().into() + } + + #[wasm_bindgen(js_name = "setUndisclosed")] + pub fn set_undisclosed(&mut self, index: usize) { + self.0.set_undisclosed(index) + } + + #[wasm_bindgen(js_name = "replacePayloadAtIndex")] + pub fn replace_payload_at_index(&mut self, index: usize, value: JsValue) -> Result { + let value = value.into_serde().wasm_result()?; + self + .0 + .replace_payload_at_index(index, value) + .map_err(|_| { + JsValue::from(WasmError::new( + Cow::Borrowed("Index out of bounds"), + Cow::Borrowed("The provided index exceeds the array's bounds"), + )) + }) + .and_then(|v| JsValue::from_serde(&v).wasm_result()) + } +} + +impl From for WasmPayloads { + fn from(value: Payloads) -> Self { + WasmPayloads(value) + } +} + +impl From for Payloads { + fn from(value: WasmPayloads) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/src/jpt/presentation_protected_header.rs new file mode 100644 index 0000000000..398870da4c --- /dev/null +++ b/bindings/wasm/src/jpt/presentation_protected_header.rs @@ -0,0 +1,86 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use jsonprooftoken::jpa::algs::PresentationProofAlgorithm; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use wasm_bindgen::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[wasm_bindgen(js_name = PresentationProofAlgorithm)] +#[allow(non_camel_case_types)] +pub enum WasmPresentationProofAlgorithm { + BLS12381_SHA256_PROOF, + BLS12381_SHAKE256_PROOF, + SU_ES256, + MAC_H256, + MAC_H384, + MAC_H512, + MAC_K25519, + MAC_K448, + MAC_H256K, +} + +impl From for PresentationProofAlgorithm { + fn from(value: WasmPresentationProofAlgorithm) -> Self { + match value { + WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF => PresentationProofAlgorithm::BLS12381_SHA256_PROOF, + WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF, + WasmPresentationProofAlgorithm::SU_ES256 => PresentationProofAlgorithm::SU_ES256, + WasmPresentationProofAlgorithm::MAC_H256 => PresentationProofAlgorithm::MAC_H256, + WasmPresentationProofAlgorithm::MAC_H384 => PresentationProofAlgorithm::MAC_H384, + WasmPresentationProofAlgorithm::MAC_H512 => PresentationProofAlgorithm::MAC_H512, + WasmPresentationProofAlgorithm::MAC_K25519 => PresentationProofAlgorithm::MAC_K25519, + WasmPresentationProofAlgorithm::MAC_K448 => PresentationProofAlgorithm::MAC_K448, + WasmPresentationProofAlgorithm::MAC_H256K => PresentationProofAlgorithm::MAC_H256K, + } + } +} + +impl From for WasmPresentationProofAlgorithm { + fn from(value: PresentationProofAlgorithm) -> Self { + match value { + PresentationProofAlgorithm::BLS12381_SHA256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF, + PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF, + PresentationProofAlgorithm::SU_ES256 => WasmPresentationProofAlgorithm::SU_ES256, + PresentationProofAlgorithm::MAC_H256 => WasmPresentationProofAlgorithm::MAC_H256, + PresentationProofAlgorithm::MAC_H384 => WasmPresentationProofAlgorithm::MAC_H384, + PresentationProofAlgorithm::MAC_H512 => WasmPresentationProofAlgorithm::MAC_H512, + PresentationProofAlgorithm::MAC_K25519 => WasmPresentationProofAlgorithm::MAC_K25519, + PresentationProofAlgorithm::MAC_K448 => WasmPresentationProofAlgorithm::MAC_K448, + PresentationProofAlgorithm::MAC_H256K => WasmPresentationProofAlgorithm::MAC_H256K, + } + } +} + +#[wasm_bindgen(js_name = PresentationProtectedHeader, inspectable, getter_with_clone)] +pub struct WasmPresentationProtectedHeader { + pub alg: WasmPresentationProofAlgorithm, + /// ID for the key used for the JWP. + pub kid: Option, + /// Who have received the JPT. + pub aud: Option, + /// For replay attacks. + pub nonce: Option, +} + +impl From for PresentationProtectedHeader { + fn from(value: WasmPresentationProtectedHeader) -> Self { + let WasmPresentationProtectedHeader { alg, kid, aud, nonce } = value; + let mut protected_header = PresentationProtectedHeader::new(alg.into()); + protected_header.set_kid(kid); + protected_header.set_aud(aud); + protected_header.set_nonce(nonce); + protected_header + } +} + +impl From for WasmPresentationProtectedHeader { + fn from(value: PresentationProtectedHeader) -> Self { + let alg = value.alg().into(); + let kid = value.kid().cloned(); + let aud = value.aud().cloned(); + let nonce = value.nonce().cloned(); + + WasmPresentationProtectedHeader { alg, kid, aud, nonce } + } +} diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs new file mode 100644 index 0000000000..0f7f6986f1 --- /dev/null +++ b/bindings/wasm/src/jpt/proof_algorithm.rs @@ -0,0 +1,52 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use wasm_bindgen::prelude::*; + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[wasm_bindgen(js_name = ProofAlgorithm)] +pub enum WasmProofAlgorithm { + BLS12381_SHA256, + BLS12381_SHAKE256, + SU_ES256, + MAC_H256, + MAC_H384, + MAC_H512, + MAC_K25519, + MAC_K448, + MAC_H256K, +} + +impl From for WasmProofAlgorithm { + fn from(value: ProofAlgorithm) -> Self { + match value { + ProofAlgorithm::BLS12381_SHA256 => WasmProofAlgorithm::BLS12381_SHA256, + ProofAlgorithm::BLS12381_SHAKE256 => WasmProofAlgorithm::BLS12381_SHAKE256, + ProofAlgorithm::SU_ES256 => WasmProofAlgorithm::SU_ES256, + ProofAlgorithm::MAC_H256 => WasmProofAlgorithm::MAC_H256, + ProofAlgorithm::MAC_H384 => WasmProofAlgorithm::MAC_H384, + ProofAlgorithm::MAC_H512 => WasmProofAlgorithm::MAC_H512, + ProofAlgorithm::MAC_K25519 => WasmProofAlgorithm::MAC_K25519, + ProofAlgorithm::MAC_K448 => WasmProofAlgorithm::MAC_K448, + ProofAlgorithm::MAC_H256K => WasmProofAlgorithm::MAC_H256K, + } + } +} + +impl From for ProofAlgorithm { + fn from(value: WasmProofAlgorithm) -> Self { + match value { + WasmProofAlgorithm::BLS12381_SHA256 => ProofAlgorithm::BLS12381_SHA256, + WasmProofAlgorithm::BLS12381_SHAKE256 => ProofAlgorithm::BLS12381_SHAKE256, + WasmProofAlgorithm::SU_ES256 => ProofAlgorithm::SU_ES256, + WasmProofAlgorithm::MAC_H256 => ProofAlgorithm::MAC_H256, + WasmProofAlgorithm::MAC_H384 => ProofAlgorithm::MAC_H384, + WasmProofAlgorithm::MAC_H512 => ProofAlgorithm::MAC_H512, + WasmProofAlgorithm::MAC_K25519 => ProofAlgorithm::MAC_K25519, + WasmProofAlgorithm::MAC_K448 => ProofAlgorithm::MAC_K448, + WasmProofAlgorithm::MAC_H256K => ProofAlgorithm::MAC_H256K, + } + } +} diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 208edca0d0..cf8344925a 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -24,6 +24,7 @@ pub mod did; pub mod error; pub mod iota; pub mod jose; +pub mod jpt; pub mod resolver; pub mod revocation; pub mod sd_jwt; diff --git a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs new file mode 100644 index 0000000000..9529128e20 --- /dev/null +++ b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs @@ -0,0 +1,69 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::storage::ProofUpdateCtx; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = ProofUpdateCtx, inspectable, getter_with_clone)] +pub struct WasmProofUpdateCtx { + /// Old `startValidityTimeframe` value + pub old_start_validity_timeframe: Vec, + /// New `startValidityTimeframe` value to be signed + pub new_start_validity_timeframe: Vec, + /// Old `endValidityTimeframe` value + pub old_end_validity_timeframe: Vec, + /// New `endValidityTimeframe` value to be signed + pub new_end_validity_timeframe: Vec, + /// Index of `startValidityTimeframe` claim inside the array of Claims + pub index_start_validity_timeframe: usize, + /// Index of `endValidityTimeframe` claim inside the array of Claims + pub index_end_validity_timeframe: usize, + /// Number of signed messages, number of payloads in a JWP + pub number_of_signed_messages: usize, +} + +impl From for WasmProofUpdateCtx { + fn from(value: ProofUpdateCtx) -> Self { + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = value; + Self { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } + } +} + +impl From for ProofUpdateCtx { + fn from(value: WasmProofUpdateCtx) -> Self { + let WasmProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = value; + Self { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } + } +} diff --git a/bindings/wasm/src/storage/jwk_storage.rs b/bindings/wasm/src/storage/jwk_storage.rs index 4616def075..6adf78845b 100644 --- a/bindings/wasm/src/storage/jwk_storage.rs +++ b/bindings/wasm/src/storage/jwk_storage.rs @@ -50,6 +50,9 @@ extern "C" { #[wasm_bindgen(method)] pub fn exists(this: &WasmJwkStorage, key_id: String) -> PromiseBool; + + #[wasm_bindgen(method)] + pub(crate) fn _get_key(this: &WasmJwkStorage, key_id: &str) -> Option; } #[async_trait::async_trait(?Send)] diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..d92f12e607 --- /dev/null +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,132 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::error::Result as WasmResult; +use crate::error::WasmResult as _; +use crate::jose::WasmJwk; +use crate::jpt::WasmProofAlgorithm; + +use super::WasmJwkGenOutput; +use super::WasmJwkStorage; +use super::WasmProofUpdateCtx; + +use identity_iota::storage::bls::encode_bls_jwk; +use identity_iota::storage::bls::expand_bls_jwk; +use identity_iota::storage::bls::generate_bbs_keypair; +use identity_iota::storage::bls::sign_bbs; +use identity_iota::storage::bls::update_bbs_signature; +use identity_iota::storage::JwkGenOutput; +use identity_iota::storage::JwkStorage; +use identity_iota::storage::JwkStorageBbsPlusExt; +use identity_iota::storage::KeyId; +use identity_iota::storage::KeyStorageError; +use identity_iota::storage::KeyStorageErrorKind; +use identity_iota::storage::KeyStorageResult; +use identity_iota::storage::KeyType; +use identity_iota::storage::ProofUpdateCtx; +use identity_iota::verification::jwk::Jwk; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_class = JwkStorage)] +impl WasmJwkStorage { + #[wasm_bindgen(js_name = generateBBS)] + /// Generates a new BBS+ keypair. + pub async fn _generate_bbs(&self, alg: WasmProofAlgorithm) -> WasmResult { + self + .generate_bbs(KeyType::from_static_str("BLS12381"), alg.into()) + .await + .map(WasmJwkGenOutput::from) + .wasm_result() + } + + #[wasm_bindgen(js_name = signBBS)] + pub async fn _sign_bbs( + &self, + key_id: String, + data: Vec, + public_key: WasmJwk, + header: Option>, + ) -> WasmResult { + let key_id = KeyId::new(key_id); + let data = data.into_iter().map(|arr| arr.to_vec()).collect::>(); + let header = header.unwrap_or_default(); + self + .sign_bbs(&key_id, &data, header.as_slice(), &public_key.into()) + .await + .map(|v| js_sys::Uint8Array::from(v.as_slice())) + .wasm_result() + } + + #[wasm_bindgen(js_name = updateBBSSignature)] + pub async fn _update_signature( + &self, + key_id: String, + public_key: &WasmJwk, + signature: Vec, + ctx: WasmProofUpdateCtx, + ) -> WasmResult { + let key_id = KeyId::new(key_id); + self + .update_signature(&key_id, &public_key.0, &signature, ctx.into()) + .await + .map(|sig| js_sys::Uint8Array::from(sig.as_slice())) + .wasm_result() + } +} + +#[async_trait::async_trait(?Send)] +impl JwkStorageBbsPlusExt for WasmJwkStorage { + async fn generate_bbs(&self, _key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let (sk, pk) = generate_bbs_keypair(alg)?; + + let (jwk, public_jwk) = encode_bls_jwk(&sk, &pk, alg); + let kid = ::insert(self, jwk).await?; + + Ok(JwkGenOutput::new(kid, public_jwk)) + } + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + }; + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + let (sk, pk) = expand_bls_jwk(&private_jwk)?; + sign_bbs(alg, data, &sk.expect("jwk was private"), &pk, header) + } + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + }; + let sk = expand_bls_jwk(&private_jwk)?.0.expect("jwk is private"); + update_bbs_signature(alg, signature, &sk, &ctx) + } +} diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs index 8295d95e88..fe54110e9d 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/src/storage/mod.rs @@ -1,14 +1,17 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod jpt_timeframe_revocation_ext; mod jwk_gen_output; mod jwk_storage; +mod jwk_storage_bbs_plus_ext; mod jwt_presentation_options; mod key_id_storage; mod method_digest; mod signature_options; mod wasm_storage; +pub use jpt_timeframe_revocation_ext::*; pub use jwk_gen_output::*; pub use jwk_storage::*; pub use jwt_presentation_options::*; diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 48d947a7ff..864041f3e3 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -110,6 +110,8 @@ async fn main() -> anyhow::Result<()> { // Publish the updated Alias Output. issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + println!("DID Document > {issuer_document:#}"); + // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ "id": alice_document.id().as_str(), diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs new file mode 100644 index 0000000000..a78dea0e76 --- /dev/null +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -0,0 +1,534 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use examples::FAUCET_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::json; +use identity_iota::core::Duration; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::core::Timestamp; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::DecodedJwtPresentation; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jpt; +use identity_iota::credential::JptCredentialValidationOptions; +use identity_iota::credential::JptCredentialValidator; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::credential::JptPresentationValidationOptions; +use identity_iota::credential::JptPresentationValidator; +use identity_iota::credential::JptPresentationValidatorUtils; +use identity_iota::credential::JwpCredentialOptions; +use identity_iota::credential::JwpPresentationOptions; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::JwtValidationError; +use identity_iota::credential::Presentation; +use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::RevocationBitmap; +use identity_iota::credential::RevocationTimeframeStatus; +use identity_iota::credential::SelectiveDisclosurePresentation; +use identity_iota::credential::Status; +use identity_iota::credential::StatusCheck; +use identity_iota::credential::Subject; +use identity_iota::credential::SubjectHolderRelationship; +use identity_iota::did::CoreDID; +use identity_iota::did::DIDUrl; +use identity_iota::did::DID; +use identity_iota::document::verifiable::JwsVerificationOptions; +use identity_iota::document::Service; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::NetworkName; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwpDocumentExt; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; +use identity_iota::storage::TimeframeRevocationExtension; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::output::AliasOutput; +use iota_sdk::types::block::output::AliasOutputBuilder; +use iota_sdk::types::block::output::RentStructure; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::thread; +use std::time::Duration as SleepDuration; + +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + key_type: KeyType, + alg: Option, + proof_alg: Option, +) -> anyhow::Result<(Address, IotaDocument, String)> { + // Get an address with funds for testing. + let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; + + // Get the Bech32 human-readable part (HRP) of the network. + let network_name: NetworkName = client.network_name().await?; + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + let mut document: IotaDocument = IotaDocument::new(&network_name); + + // New Verification Method containing a BBS+ key + let fragment = if let Some(alg) = alg { + document + .generate_method(storage, key_type, alg, None, MethodScope::VerificationMethod) + .await? + } else if let Some(proof_alg) = proof_alg { + let fragment = document + .generate_method_jwp(storage, key_type, proof_alg, None, MethodScope::VerificationMethod) + .await?; + + // Create a new empty revocation bitmap. No credential is revoked yet. + let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); + + // Add the revocation bitmap to the DID document of the issuer as a service. + let service_id: DIDUrl = document.id().to_url().join("#my-revocation-service")?; + let service: Service = revocation_bitmap.to_service(service_id)?; + + assert!(document.insert_service(service).is_ok()); + + fragment + } else { + return Err(anyhow::Error::msg("You have to pass at least one algorithm")); + }; + + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + + // Publish the Alias Output and get the published DID document. + let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + println!("Published DID document: {document:#}"); + + Ok((address, document, fragment)) +} + +/// Demonstrates how to create an Anonymous Credential with BBS+. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + + let secret_manager_issuer = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_1".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let secret_manager_holder = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_2".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_holder: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_issuer, + &storage_issuer, + JwkMemStore::BLS12381G2_KEY_TYPE, + None, + Some(ProofAlgorithm::BLS12381_SHA256), + ) + .await?; + + let (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_holder, + &storage_holder, + JwkMemStore::ED25519_KEY_TYPE, + Some(JwsAlgorithm::EdDSA), + None, + ) + .await?; + + // Create a credential subject indicating the degree earned by Alice. + let subject: Subject = Subject::from_json_value(json!({ + "id": holder_document.id().as_str(), + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // ========================================================================================= + // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe + // ======================================================================================= + let duration = Duration::minutes(1); + // The issuer also chooses a unique `RevocationBitmap` index to be able to revoke it later. + let service_url = issuer_document.id().to_url().join("#my-revocation-service")?; + let credential_index: u32 = 5; + + let start_validity_timeframe = Timestamp::now_utc(); + let status: Status = RevocationTimeframeStatus::new( + Some(start_validity_timeframe), + duration, + service_url.into(), + credential_index, + )? + .into(); + + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer_document.id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .status(status) + .build()?; + + let credential_jpt: Jpt = issuer_document + .create_credential_jpt( + &credential, + &storage_issuer, + &fragment_issuer, + &JwpCredentialOptions::default(), + None, + ) + .await?; + + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + let decoded_jpt = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + assert_eq!(credential, decoded_jpt.credential); + + // Issuer sends the Verifiable Credential to the holder. + println!( + "Sending credential (as JPT) to the holder: {}\n", + credential_jpt.as_str() + ); + + // Holder validate the credential and retrieve the JwpIssued, needed to construct the JwpPresented + + let validation_result = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ); + + let decoded_credential = validation_result.unwrap(); + + // =========================================================================== + // Credential's Status check + // =========================================================================== + + // Timeframe check + let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_credential.credential, + None, + StatusCheck::Strict, + ); + + assert!(timeframe_result.is_ok()); + + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + StatusCheck::Strict, + ); + + assert!(revocation_result.is_ok()); + + // Both checks + + let revocation_result = JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + None, + StatusCheck::Strict, + ); + + assert!(revocation_result.is_ok()); + + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + let method_id = decoded_credential + .decoded_jwp + .get_issuer_protected_header() + .kid() + .unwrap(); + + let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp); + selective_disclosure_presentation + .conceal_in_subject("mainCourses[1]") + .unwrap(); + selective_disclosure_presentation + .conceal_in_subject("degree.name") + .unwrap(); + + let presentation_jpt: Jpt = issuer_document + .create_presentation_jpt( + &mut selective_disclosure_presentation, + method_id, + &JwpPresentationOptions::default().nonce(challenge), + ) + .await?; + + // Holder sends a Presentation JPT to the Verifier. + println!( + "Sending presentation (as JPT) to the verifier: {}\n", + presentation_jpt.as_str() + ); + + // =========================================================================== + // Step 2a: Verifier receives the Presentation and verifies it. + // =========================================================================== + + let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); + + // Verifier validate the Presented Credential and retrieve the JwpPresented + let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>( + &presentation_jpt, + &issuer_document, + &presentation_validation_options, + FailFast::FirstError, + ) + .unwrap(); + + // Check validityTimeframe + + let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_presented_credential.credential, + None, + StatusCheck::Strict, + ); + + assert!(timeframe_result.is_ok()); + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!( + "Presented Credential successfully validated: {:#}", + decoded_presented_credential.credential + ); + + // =========================================================================== + // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid + // =========================================================================== + + thread::sleep(SleepDuration::from_secs(61)); + + let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_presented_credential.credential, + None, + StatusCheck::Strict, + ); + + // We expect validation to no longer succeed because the credential was NOT updated. + if matches!(timeframe_result.unwrap_err(), JwtValidationError::OutsideTimeframe) { + println!("Validity Timeframe interval NOT valid\n"); + } + + // =========================================================================== + // 3: Update credential + // =========================================================================== + + // =========================================================================== + // 3.1: Issuer sends the holder a challenge and requests a signed Verifiable Presentation. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // The Holder and Issuer also agree that the signature should have an expiry date + // 10 minutes from now. + let expires: Timestamp = Timestamp::now_utc().checked_add(Duration::minutes(10)).unwrap(); + + // =========================================================================== + // 3.2: Holder creates and signs a verifiable presentation from the issued credential. + // =========================================================================== + + // Create an unsigned Presentation from the previously issued ZK Verifiable Credential. + let presentation: Presentation = + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) + .credential(credential_jpt) + .build()?; + + // Create a JWT verifiable presentation using the holder's verification method + // and include the requested challenge and expiry timestamp. + let presentation_jwt: Jwt = holder_document + .create_presentation_jwt( + &presentation, + &storage_holder, + &fragment_holder, + &JwsSignatureOptions::default().nonce(challenge.to_owned()), + &JwtPresentationOptions::default().expiration_date(expires), + ) + .await?; + + // =========================================================================== + // 3.3: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + println!( + "Sending presentation (as JWT) to the Issuer: {}\n", + presentation_jwt.as_str() + ); + + // =========================================================================== + // 3.4: Issuer validate Verifiable Presentation and ZK Verifiable Credential. + // =========================================================================== + + // ================================================ + // 3.4.1: Issuer validate Verifiable Presentation. + // ================================================ + + let presentation_verifier_options: JwsVerificationOptions = + JwsVerificationOptions::default().nonce(challenge.to_owned()); + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + + // Resolve the holder's document. + let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; + let holder: IotaDocument = resolver.resolve(&holder_did).await?; + + // Validate presentation. Note that this doesn't validate the included credentials. + let presentation_validation_options = + JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options); + let presentation: DecodedJwtPresentation = JwtPresentationValidator::with_signature_verifier( + EdDSAJwsVerifier::default(), + ) + .validate(&presentation_jwt, &holder, &presentation_validation_options)?; + + // ======================================================================= + // 3.4.2: Issuer validate ZK Verifiable Credential inside the Presentation. + // ======================================================================== + + let validation_options: JptCredentialValidationOptions = JptCredentialValidationOptions::default() + .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject); + + let jpt_credentials: &Vec = &presentation.presentation.verifiable_credential; + + // Extract ZK Verifiable Credential in JPT format + let jpt_vc = jpt_credentials.first().unwrap(); + + // Issuer checks the Credential integrity. + let mut verified_credential_result = + JptCredentialValidator::validate::<_, Object>(jpt_vc, &issuer_document, &validation_options, FailFast::FirstError) + .unwrap(); + + // Issuer checks if the Credential has been revoked + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &verified_credential_result.credential, + &issuer_document, + StatusCheck::Strict, + ); + + assert!(!revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + + // =========================================================================== + // 3.5: Issuer ready for Update. + // =========================================================================== + + // Since no errors were thrown during the Verifiable Presentation validation and the verification of inner Credentials + println!( + "Ready for Update - VP successfully validated: {:#?}", + presentation.presentation + ); + + // Issuer updates the credential + let new_credential_jpt = issuer_document + .update( + &storage_issuer, + &fragment_issuer, + None, + duration, + &mut verified_credential_result.decoded_jwp, + ) + .await?; + + // Issuer sends back the credential updated + + println!( + "Sending updated credential (as JPT) to the holder: {}\n", + new_credential_jpt.as_str() + ); + + // Holder check validity of the updated credential + + let validation_result = JptCredentialValidator::validate::<_, Object>( + &new_credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &validation_result.credential, + None, + StatusCheck::Strict, + ); + + assert!(!timeframe_result + .as_ref() + .is_err_and(|e| matches!(e, JwtValidationError::OutsideTimeframe))); + println!("Updated credential is VALID!"); + + // =========================================================================== + // Issuer decides to Revoke Holder's Credential + // =========================================================================== + + println!("Issuer decides to revoke the Credential"); + + // Update the RevocationBitmap service in the issuer's DID Document. + // This revokes the credential's unique index. + + issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; + + // Publish the changes. + let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; + let rent_structure: RentStructure = client.get_rent_structure().await?; + let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) + .with_minimum_storage_deposit(rent_structure) + .finish()?; + issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + + // Holder checks if his credential has been revoked by the Issuer + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + StatusCheck::Strict, + ); + assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + println!("Credential Revoked!"); + Ok(()) +} diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs new file mode 100644 index 0000000000..eeb4246280 --- /dev/null +++ b/examples/1_advanced/9_zkp.rs @@ -0,0 +1,260 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use examples::FAUCET_ENDPOINT; +use identity_iota::core::json; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jpt; +use identity_iota::credential::JptCredentialValidationOptions; +use identity_iota::credential::JptCredentialValidator; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::credential::JptPresentationValidationOptions; +use identity_iota::credential::JptPresentationValidator; +use identity_iota::credential::JptPresentationValidatorUtils; +use identity_iota::credential::JwpCredentialOptions; +use identity_iota::credential::JwpPresentationOptions; +use identity_iota::credential::SelectiveDisclosurePresentation; +use identity_iota::credential::Subject; +use identity_iota::did::CoreDID; +use identity_iota::did::DID; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::NetworkName; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwpDocumentExt; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; +use identity_iota::verification::MethodScope; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::output::AliasOutput; +use jsonprooftoken::jpa::algs::ProofAlgorithm; + +// Creates a DID with a JWP verification method. +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + key_type: KeyType, + alg: ProofAlgorithm, +) -> anyhow::Result<(Address, IotaDocument, String)> { + // Get an address with funds for testing. + let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; + + // Get the Bech32 human-readable part (HRP) of the network. + let network_name: NetworkName = client.network_name().await?; + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + let mut document: IotaDocument = IotaDocument::new(&network_name); + + let fragment = document + .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod) + .await?; + + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + + // Publish the Alias Output and get the published DID document. + let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + println!("Published DID document: {document:#}"); + + Ok((address, document, fragment)) +} + +/// Demonstrates how to create an Anonymous Credential with BBS+. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // =========================================================================== + // Step 1: Create identity for the issuer. + // =========================================================================== + + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + + let secret_manager_issuer = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_1".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_issuer, + &storage_issuer, + JwkMemStore::BLS12381G2_KEY_TYPE, + ProofAlgorithm::BLS12381_SHA256, + ) + .await?; + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm. + // =========================================================================== + + // Create a credential subject indicating the degree earned by Alice. + let subject: Subject = Subject::from_json_value(json!({ + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer_document.id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .build()?; + + let credential_jpt: Jpt = issuer_document + .create_credential_jpt( + &credential, + &storage_issuer, + &fragment_issuer, + &JwpCredentialOptions::default(), + None, + ) + .await?; + + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + let decoded_jpt = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + assert_eq!(credential, decoded_jpt.credential); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + println!( + "Sending credential (as JPT) to the holder: {}\n", + credential_jpt.as_str() + ); + + // ============================================================================================ + // Step 4: Holder resolves Issuer's DID, retrieve Issuer's document and validate the Credential + // ============================================================================================ + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + + // Holder resolves issuer's DID + let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decoded_credential = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + // =========================================================================== + // Step 5: Verifier sends the holder a challenge and requests a Presentation. + // + // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations). + // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // ========================================================================================================= + // Step 6: Holder engages in the Selective Disclosure of credential's attributes. + // ========================================================================================================= + + let method_id = decoded_credential + .decoded_jwp + .get_issuer_protected_header() + .kid() + .unwrap(); + + let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp); + selective_disclosure_presentation + .conceal_in_subject("mainCourses[1]") + .unwrap(); + selective_disclosure_presentation + .conceal_in_subject("degree.name") + .unwrap(); + + // ======================================================================================================================================= + // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation + // JPT. + // ======================================================================================================================================= + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + let presentation_jpt: Jpt = issuer_document + .create_presentation_jpt( + &mut selective_disclosure_presentation, + method_id, + &JwpPresentationOptions::default().nonce(challenge), + ) + .await?; + + // =========================================================================== + // Step 8: Holder sends a Presentation JPT to the Verifier. + // =========================================================================== + + println!( + "Sending presentation (as JPT) to the verifier: {}\n", + presentation_jpt.as_str() + ); + + // =========================================================================== + // Step 9: Verifier receives the Presentation and verifies it. + // =========================================================================== + + // Verifier resolve Issuer DID + let issuer: CoreDID = JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation_jpt).unwrap(); + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + + let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); + + // Verifier validate the Presented Credential and retrieve the JwpPresented + let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>( + &presentation_jpt, + &issuer_document, + &presentation_validation_options, + FailFast::FirstError, + ) + .unwrap(); + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!( + "Presented Credential successfully validated: {:#?}", + decoded_presented_credential.credential + ); + + Ok(()) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e91dad0eca..4e107cb042 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,9 +8,10 @@ publish = false [dependencies] anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } -identity_stronghold = { path = "../identity_stronghold", default-features = false } +identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] } +identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } +json-proof-token.workspace = true primitive-types = "0.12.1" rand = "0.8.5" sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] } @@ -91,3 +92,11 @@ name = "7_sd_jwt" [[example]] path = "1_advanced/8_status_list_2021.rs" name = "8_status_list_2021" + +[[example]] +path = "1_advanced/9_zkp.rs" +name = "9_zkp" + +[[example]] +path = "1_advanced/10_zkp_revocation.rs" +name = "10_zkp_revocation" diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index fd3323db9e..91834c6aba 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] +async-trait = { version = "0.1.64", default-features = false } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true } identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } @@ -20,17 +21,19 @@ identity_document = { version = "=1.2.0", path = "../identity_document", default identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } +json-proof-token = { workspace = true, optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } serde.workspace = true -serde-aux = { version = "4.3.1", default-features = false, optional = true } +serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } +zkryptium = { workspace = true, optional = true } [dev-dependencies] anyhow = "1.0.62" @@ -50,11 +53,12 @@ default = ["revocation-bitmap", "validator", "credential", "presentation", "doma credential = [] presentation = ["credential"] revocation-bitmap = ["dep:flate2", "dep:roaring"] -status-list-2021 = ["revocation-bitmap", "dep:serde-aux"] +status-list-2021 = ["revocation-bitmap"] validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] -sd-jwt = ["credential", "validator", "sd-jwt-payload"] +sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] +jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:json-proof-token"] [lints] workspace = true diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/credential.rs index decbb8b7c2..03c482c6f6 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/credential.rs @@ -5,6 +5,8 @@ use core::fmt::Display; use core::fmt::Formatter; use identity_core::convert::ToJson; +#[cfg(feature = "jpt-bbs-plus")] +use jsonprooftoken::jpt::claims::JptClaims; use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; @@ -174,6 +176,16 @@ impl Credential { .to_json() .map_err(|err| Error::JwtClaimsSetSerializationError(err.into())) } + + ///Serializes the [`Credential`] as a JPT claims set + #[cfg(feature = "jpt-bbs-plus")] + pub fn serialize_jpt(&self, custom_claims: Option) -> Result + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?; + Ok(jwt_representation.into()) + } } impl Display for Credential diff --git a/identity_credential/src/credential/jpt.rs b/identity_credential/src/credential/jpt.rs new file mode 100644 index 0000000000..feab003949 --- /dev/null +++ b/identity_credential/src/credential/jpt.rs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; + +/// This JSON Proof Token could represent a JWP both in the Issued and Presented forms. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub struct Jpt(String); + +impl Jpt { + /// Creates a new `Jwt` from the given string. + pub fn new(jpt_string: String) -> Self { + Self(jpt_string) + } + + /// Returns a reference of the JWT string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for Jpt { + fn from(jpt: String) -> Self { + Self::new(jpt) + } +} + +impl From for String { + fn from(jpt: Jpt) -> Self { + jpt.0 + } +} diff --git a/identity_credential/src/credential/jwp_credential_options.rs b/identity_credential/src/credential/jwp_credential_options.rs new file mode 100644 index 0000000000..f607c2f68e --- /dev/null +++ b/identity_credential/src/credential/jwp_credential_options.rs @@ -0,0 +1,28 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +/// Options for creating a JSON Web Proof. +#[non_exhaustive] +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct JwpCredentialOptions { + /// The kid to set in the Issuer Protected Header. + /// + /// If unset, the kid of the JWK with which the JWP is produced is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, +} + +impl JwpCredentialOptions { + /// Creates a new [`JwsSignatureOptions`]. + pub fn new() -> Self { + Self::default() + } + + /// Replace the value of the `kid` field. + pub fn kid(mut self, value: impl Into) -> Self { + self.kid = Some(value.into()); + self + } +} diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 8a9bc280e8..6ce3c60b67 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -3,6 +3,8 @@ use std::borrow::Cow; +#[cfg(feature = "jpt-bbs-plus")] +use jsonprooftoken::jpt::claims::JptClaims; use serde::Deserialize; use serde::Serialize; @@ -360,6 +362,57 @@ where proof: Option>, } +#[cfg(feature = "jpt-bbs-plus")] +impl<'credential, T> From> for JptClaims +where + T: ToOwned + Serialize, + ::Owned: DeserializeOwned, +{ + fn from(item: CredentialJwtClaims<'credential, T>) -> Self { + let CredentialJwtClaims { + exp, + iss, + issuance_date, + jti, + sub, + vc, + custom, + } = item; + + let mut claims = JptClaims::new(); + + if let Some(exp) = exp { + claims.set_exp(exp); + } + + claims.set_iss(iss.url().to_string()); + + if let Some(iat) = issuance_date.iat { + claims.set_iat(iat); + } + + if let Some(nbf) = issuance_date.nbf { + claims.set_nbf(nbf); + } + + if let Some(jti) = jti { + claims.set_jti(jti.to_string()); + } + + if let Some(sub) = sub { + claims.set_sub(sub.to_string()); + } + + claims.set_claim(Some("vc"), vc, true); + + if let Some(custom) = custom { + claims.set_claim(None, custom, true); + } + + claims + } +} + #[cfg(test)] mod tests { use identity_core::common::Object; diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index efa20a3c87..72f3b5d7a8 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -9,6 +9,10 @@ mod builder; mod credential; mod evidence; mod issuer; +#[cfg(feature = "jpt-bbs-plus")] +mod jpt; +#[cfg(feature = "jpt-bbs-plus")] +mod jwp_credential_options; mod jws; mod jwt; mod jwt_serialization; @@ -26,6 +30,10 @@ pub use self::builder::CredentialBuilder; pub use self::credential::Credential; pub use self::evidence::Evidence; pub use self::issuer::Issuer; +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jpt::Jpt; +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; pub use self::linked_domain_service::LinkedDomainService; @@ -33,6 +41,8 @@ pub use self::policy::Policy; pub use self::proof::Proof; pub use self::refresh::RefreshService; #[cfg(feature = "revocation-bitmap")] +pub use self::revocation_bitmap_status::try_index_to_u32; +#[cfg(feature = "revocation-bitmap")] pub use self::revocation_bitmap_status::RevocationBitmapStatus; pub use self::schema::Schema; pub use self::status::Status; diff --git a/identity_credential/src/credential/revocation_bitmap_status.rs b/identity_credential/src/credential/revocation_bitmap_status.rs index d4310d154a..b607e1758d 100644 --- a/identity_credential/src/credential/revocation_bitmap_status.rs +++ b/identity_credential/src/credential/revocation_bitmap_status.rs @@ -129,7 +129,7 @@ impl From for Status { } /// Attempts to convert the given index string to a u32. -fn try_index_to_u32(index: &str, name: &str) -> Result { +pub fn try_index_to_u32(index: &str, name: &str) -> Result { u32::from_str(index).map_err(|err| { Error::InvalidStatus(format!( "{name} cannot be converted to an unsigned, 32-bit integer: {err}", diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 62964415e4..468370e460 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -68,4 +68,12 @@ pub enum Error { /// JSON. #[error("could not deserialize JWT claims set")] JwtClaimsSetDeserializationError(#[source] Box), + + /// Caused by a failure to deserialize the JPT claims set representation of a `Credential` JSON. + #[error("could not deserialize JWT claims set")] + JptClaimsSetDeserializationError(#[source] Box), + + /// Cause by an invalid attribute path + #[error("Attribute Not found")] + SelectiveDisclosureError, } diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs new file mode 100644 index 0000000000..e6919058a2 --- /dev/null +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -0,0 +1,124 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use crate::error::Result; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssued; +use jsonprooftoken::jwp::presented::JwpPresentedBuilder; + +/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes. +// - @context MUST NOT be blinded +// - id MUST be blinded +// - type MUST NOT be blinded +// - issuer MUST NOT be blinded +// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +// - credentialSubject (Users have to choose which attribute must be blinded) +// - credentialSchema MUST NOT be blinded +// - credentialStatus MUST NOT be blinded +// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +// - evidence (Users have to choose which attribute must be blinded) +pub struct SelectiveDisclosurePresentation { + jwp_builder: JwpPresentedBuilder, +} + +impl SelectiveDisclosurePresentation { + /// Initialize a presentation starting from an Issued JWP. + /// The following properties are concealed by default: + /// + /// - `exp` + /// - `expirationDate` + /// - `issuanceDate` + /// - `jti` + /// - `nbf` + /// - `sub` + /// - `termsOfUse` + /// - `vc.credentialStatus.revocationBitmapIndex` + /// - `vc.credentialSubject.id` + pub fn new(issued_jwp: &JwpIssued) -> Self { + let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp); + + jwp_builder.set_undisclosed("jti").ok(); // contains the credential's id, provides linkability + + jwp_builder.set_undisclosed("issuanceDate").ok(); // Depending on the revocation method used it will be necessary or not + jwp_builder.set_undisclosed("nbf").ok(); + + jwp_builder.set_undisclosed("expirationDate").ok(); // Depending on the revocation method used it will be necessary or not + jwp_builder.set_undisclosed("exp").ok(); + + jwp_builder.set_undisclosed("termsOfUse").ok(); // Provides linkability so, there is NO reason to use it in ZK VC + + jwp_builder + .set_undisclosed("vc.credentialStatus.revocationBitmapIndex") + .ok(); + + jwp_builder.set_undisclosed("vc.credentialSubject.id").ok(); + jwp_builder.set_undisclosed("sub").ok(); + + Self { jwp_builder } + } + + /// Selectively conceal "credentialSubject" attributes. + /// # Example + /// ```ignore + /// { + /// "id": 1234, + /// "name": "Alice", + /// "mainCourses": ["Object-oriented Programming", "Mathematics"], + /// "degree": { + /// "type": "BachelorDegree", + /// "name": "Bachelor of Science and Arts", + /// }, + /// "GPA": "4.0", + /// } + /// ``` + /// If you want to undisclose for example the Mathematics course and the name of the degree: + /// ```ignore + /// presentation_builder.conceal_in_subject("mainCourses[1]"); + /// presentation_builder.conceal_in_subject("degree.name"); + /// ``` + pub fn conceal_in_subject(&mut self, path: &str) -> Result<(), Error> { + let _ = self + .jwp_builder + .set_undisclosed(&("vc.credentialSubject.".to_owned() + path)) + .map_err(|_| Error::SelectiveDisclosureError); + Ok(()) + } + + /// Undisclose "evidence" attributes. + /// # Example + /// ```ignore + /// { + /// "id": "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231", + /// "type": ["DocumentVerification"], + /// "verifier": "https://example.edu/issuers/14", + /// "evidenceDocument": "DriversLicense", + /// "subjectPresence": "Physical", + /// "documentPresence": "Physical", + /// "licenseNumber": "123AB4567" + /// } + /// ``` + /// To conceal the `licenseNumber` field: + /// ```ignore + /// presentation_builder.conceal_in_evidence("licenseNumber"); + /// ``` + pub fn conceal_in_evidence(&mut self, path: &str) -> Result<(), Error> { + let _ = self + .jwp_builder + .set_undisclosed(&("vc.evidence.".to_owned() + path)) + .map_err(|_| Error::SelectiveDisclosureError); + Ok(()) + } + + /// Set Presentation Protected Header. + pub fn set_presentation_header(&mut self, ph: PresentationProtectedHeader) { + self.jwp_builder.set_presentation_protected_header(ph); + } + + /// Get the builder. + pub fn builder(&self) -> &JwpPresentedBuilder { + &self.jwp_builder + } +} diff --git a/identity_credential/src/presentation/jwp_presentation_options.rs b/identity_credential/src/presentation/jwp_presentation_options.rs new file mode 100644 index 0000000000..fba35a7f1f --- /dev/null +++ b/identity_credential/src/presentation/jwp_presentation_options.rs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Url; +use serde::Deserialize; +use serde::Serialize; + +/// Options to be set in the JWT claims of a verifiable presentation. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct JwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWP Presentation Header). + /// Default: `None`. + #[serde(skip_serializing_if = "Option::is_none")] + pub audience: Option, + + /// The nonce to be placed in the Presentation Protected Header. + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, +} + +impl JwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWT claims). + pub fn audience(mut self, audience: Url) -> Self { + self.audience = Some(audience); + self + } + + /// Replace the value of the `nonce` field. + pub fn nonce(mut self, value: impl Into) -> Self { + self.nonce = Some(value.into()); + self + } +} diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 94f8768e02..76adc145c6 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -5,14 +5,22 @@ #![allow(clippy::module_inception)] +#[cfg(feature = "jpt-bbs-plus")] +mod jwp_presentation_builder; +#[cfg(feature = "jpt-bbs-plus")] +mod jwp_presentation_options; mod jwt_presentation_options; mod jwt_serialization; mod presentation; mod presentation_builder; +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jwp_presentation_builder::SelectiveDisclosurePresentation; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; pub use self::presentation_builder::PresentationBuilder; +#[cfg(feature = "jpt-bbs-plus")] +pub use jwp_presentation_options::JwpPresentationOptions; #[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::PresentationJwtClaims; diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index 6732ff4194..1553022c74 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -9,6 +9,11 @@ mod revocation_bitmap_2022; #[cfg(feature = "status-list-2021")] pub mod status_list_2021; +#[cfg(feature = "jpt-bbs-plus")] +pub mod validity_timeframe_2024; + pub use self::error::RevocationError; pub use self::error::RevocationResult; pub use revocation_bitmap_2022::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use validity_timeframe_2024::*; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs new file mode 100644 index 0000000000..179d5696ec --- /dev/null +++ b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of a new Revocation mechanism for ZK Verifiable Credentials. + +mod revocation_timeframe_status; + +pub use revocation_timeframe_status::*; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs new file mode 100644 index 0000000000..0a70589112 --- /dev/null +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -0,0 +1,220 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use crate::credential::Status; +use crate::error::Error; +use crate::error::Result; +use identity_core::common::Duration; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_core::common::Value; +use serde::de::Visitor; +use serde::Deserialize; +use serde::Serialize; + +fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct ExactStrVisitor(&'static str); + impl<'a> Visitor<'a> for ExactStrVisitor { + type Value = &'static str; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "the exact string \"{}\"", self.0) + } + fn visit_str(self, str: &str) -> Result { + if str == self.0 { + Ok(self.0) + } else { + Err(E::custom(format!("not \"{}\"", self.0))) + } + } + } + + deserializer + .deserialize_str(ExactStrVisitor(RevocationTimeframeStatus::TYPE)) + .map(ToOwned::to_owned) +} + +/// Information used to determine the current status of a [`Credential`][crate::credential::Credential] +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RevocationTimeframeStatus { + id: Url, + #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")] + type_: String, + start_validity_timeframe: Timestamp, + end_validity_timeframe: Timestamp, + #[serde( + deserialize_with = "serde_aux::prelude::deserialize_option_number_from_string", + skip_serializing_if = "Option::is_none" + )] + revocation_bitmap_index: Option, +} + +impl RevocationTimeframeStatus { + /// startValidityTimeframe property name. + pub const START_TIMEFRAME_PROPERTY: &'static str = "startValidityTimeframe"; + /// endValidityTimeframe property name. + pub const END_TIMEFRAME_PROPERTY: &'static str = "endValidityTimeframe"; + /// Type name of the revocation mechanism. + pub const TYPE: &'static str = "RevocationTimeframe2024"; + /// index property name for [`Status`] conversion + const INDEX_PROPERTY: &'static str = "revocationBitmapIndex"; + + /// Creates a new `RevocationTimeframeStatus`. + pub fn new(start_validity: Option, duration: Duration, id: Url, index: u32) -> Result { + let start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); + let end_validity_timeframe = start_validity_timeframe + .checked_add(duration) + .ok_or(Error::InvalidStatus( + "With that granularity, endValidityTimeFrame will turn out not to be in the valid range for RFC 3339" + .to_owned(), + ))?; + + Ok(Self { + id, + type_: Self::TYPE.to_owned(), + start_validity_timeframe, + end_validity_timeframe, + revocation_bitmap_index: Some(index), + }) + } + + /// Get startValidityTimeframe value. + pub fn start_validity_timeframe(&self) -> Timestamp { + self.start_validity_timeframe + } + + /// Get endValidityTimeframe value. + pub fn end_validity_timeframe(&self) -> Timestamp { + self.end_validity_timeframe + } + + /// Returns the [`Url`] of the `RevocationBitmapStatus`, which should resolve + /// to a `RevocationBitmap2022` service in a DID Document. + pub fn id(&self) -> &Url { + &self.id + } + + /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded. + pub fn index(&self) -> Option { + self.revocation_bitmap_index + } +} + +impl TryFrom<&Status> for RevocationTimeframeStatus { + type Error = Error; + fn try_from(status: &Status) -> Result { + // serialize into String to ensure macros work properly + // see [issue](https://github.com/iddm/serde-aux/issues/34#issuecomment-1508207530) in `serde-aux` + let json_status: String = serde_json::to_string(&status) + .map_err(|err| Self::Error::InvalidStatus(format!("failed to read `Status`; {}", &err.to_string())))?; + serde_json::from_str(&json_status).map_err(|err| { + Self::Error::InvalidStatus(format!( + "failed to convert `Status` to `RevocationTimeframeStatus`; {}", + &err.to_string(), + )) + }) + } +} + +impl From for Status { + fn from(revocation_timeframe_status: RevocationTimeframeStatus) -> Self { + let mut properties = Object::new(); + properties.insert( + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY.to_owned(), + Value::String(revocation_timeframe_status.start_validity_timeframe().to_rfc3339()), + ); + properties.insert( + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY.to_owned(), + Value::String(revocation_timeframe_status.end_validity_timeframe().to_rfc3339()), + ); + if let Some(value) = revocation_timeframe_status.index() { + properties.insert( + RevocationTimeframeStatus::INDEX_PROPERTY.to_owned(), + Value::String(value.to_string()), + ); + } + + Status::new_with_properties( + revocation_timeframe_status.id, + RevocationTimeframeStatus::TYPE.to_owned(), + properties, + ) + } +} + +/// Verifier +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifierRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus); + +impl TryFrom for VerifierRevocationTimeframeStatus { + type Error = Error; + + fn try_from(status: Status) -> Result { + Ok(Self((&status).try_into().map_err(|err: Error| { + Self::Error::InvalidStatus(format!( + "failed to convert `Status` to `VerifierRevocationTimeframeStatus`; {}", + &err.to_string() + )) + })?)) + } +} + +impl From for Status { + fn from(status: VerifierRevocationTimeframeStatus) -> Self { + status.0.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EXAMPLE_SERIALIZED: &str = r#"{ + "id": "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service", + "startValidityTimeframe": "2024-03-19T13:57:50Z", + "endValidityTimeframe": "2024-03-19T13:58:50Z", + "revocationBitmapIndex": "5", + "type": "RevocationTimeframe2024" + }"#; + + fn get_example_status() -> anyhow::Result { + let duration = Duration::minutes(1); + let service_url = Url::parse( + "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service", + )?; + let credential_index: u32 = 5; + let start_validity_timeframe = Timestamp::parse("2024-03-19T13:57:50Z")?; + + Ok(RevocationTimeframeStatus::new( + Some(start_validity_timeframe), + duration, + service_url, + credential_index, + )?) + } + + #[test] + fn revocation_timeframe_status_serialization_works() -> anyhow::Result<()> { + let status = get_example_status()?; + + let serialized = serde_json::to_string(&status).expect("Failed to deserialize"); + dbg!(&serialized); + + Ok(()) + } + + #[test] + fn revocation_timeframe_status_deserialization_works() -> anyhow::Result<()> { + let status = get_example_status()?; + let deserialized = + serde_json::from_str::(EXAMPLE_SERIALIZED).expect("Failed to deserialize"); + + assert_eq!(status, deserialized); + + Ok(()) + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs new file mode 100644 index 0000000000..b574abfa13 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs @@ -0,0 +1,19 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use jsonprooftoken::jwp::issued::JwpIssued; + +use crate::credential::Credential; + +/// Decoded [`Credential`] from a cryptographically verified JWP. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJptCredential { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub credential: Credential, + /// The custom claims parsed from the JPT. + pub custom_claims: Option, + /// The decoded and verifier Issued JWP, will be used to construct the Presented JWP + pub decoded_jwp: JwpIssued, +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs new file mode 100644 index 0000000000..2cbaafac28 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs @@ -0,0 +1,87 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use crate::validator::SubjectHolderRelationship; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_document::verifiable::JwpVerificationOptions; +use serde::Deserialize; +use serde::Serialize; + +/// Options to declare validation criteria for [`Credential`](crate::credential::Credential)s. +#[non_exhaustive] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JptCredentialValidationOptions { + /// Declares that the credential is **not** considered valid if it expires before this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. + #[serde(default)] + pub earliest_expiry_date: Option, + + /// Declares that the credential is **not** considered valid if it was issued later than this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. + #[serde(default)] + pub latest_issuance_date: Option, + + /// Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + /// + /// Default: [`StatusCheck::Strict`](crate::validator::StatusCheck::Strict). + #[serde(default)] + pub status: crate::validator::StatusCheck, + + /// Declares how credential subjects must relate to the presentation holder during validation. + /// + /// + pub subject_holder_relationship: Option<(Url, SubjectHolderRelationship)>, + + /// Options which affect the verification of the proof on the credential. + #[serde(default)] + pub verification_options: JwpVerificationOptions, +} + +impl JptCredentialValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Declare that the credential is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn earliest_expiry_date(mut self, timestamp: Timestamp) -> Self { + self.earliest_expiry_date = Some(timestamp); + self + } + + /// Declare that the credential is **not** considered valid if it was issued later than this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn latest_issuance_date(mut self, timestamp: Timestamp) -> Self { + self.latest_issuance_date = Some(timestamp); + self + } + + /// Sets the validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + pub fn status_check(mut self, status_check: crate::validator::StatusCheck) -> Self { + self.status = status_check; + self + } + + /// Declares how credential subjects must relate to the presentation holder during validation. + /// + /// + pub fn subject_holder_relationship( + mut self, + holder: Url, + subject_holder_relationship: SubjectHolderRelationship, + ) -> Self { + self.subject_holder_relationship = Some((holder, subject_holder_relationship)); + self + } + + /// Set options which affect the verification of the JWP proof. + pub fn verification_options(mut self, options: JwpVerificationOptions) -> Self { + self.verification_options = options; + self + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs new file mode 100644 index 0000000000..3639d1a229 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs @@ -0,0 +1,225 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_document::verifiable::JwpVerificationOptions; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwp::issued::JwpIssuedDecoder; + +use super::DecodedJptCredential; +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::jwt_credential_validation::SignerContext; +use crate::validator::CompoundCredentialValidationError; +use crate::validator::FailFast; +use crate::validator::JptCredentialValidationOptions; +use crate::validator::JwtCredentialValidatorUtils; +use crate::validator::JwtValidationError; + +/// A type for decoding and validating [`Credential`]s in JPT format. +#[non_exhaustive] +pub struct JptCredentialValidator; + +impl JptCredentialValidator { + /// Decodes and validates a [`Credential`] issued as a JPT (JWP Issued Form). A [`DecodedJptCredential`] is returned + /// upon success. + /// + /// The following properties are validated according to `options`: + /// - the issuer's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + pub fn validate( + credential_jpt: &Jpt, + issuer: &DOC, + options: &JptCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + // First verify the JWP proof and decode the result into a credential token, then apply all other validations. + let credential_token = + Self::verify_proof(credential_jpt, issuer, &options.verification_options).map_err(|err| { + CompoundCredentialValidationError { + validation_errors: [err].into(), + } + })?; + + let credential: &Credential = &credential_token.credential; + + Self::validate_credential::(credential, options, fail_fast)?; + + Ok(credential_token) + } + + pub(crate) fn validate_credential( + credential: &Credential, + options: &JptCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result<(), CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Run all single concern Credential validations in turn and fail immediately if `fail_fast` is true. + let expiry_date_validation = std::iter::once_with(|| { + JwtCredentialValidatorUtils::check_expires_on_or_after( + credential, + options.earliest_expiry_date.unwrap_or_default(), + ) + }); + + let issuance_date_validation = std::iter::once_with(|| { + JwtCredentialValidatorUtils::check_issued_on_or_before( + credential, + options.latest_issuance_date.unwrap_or_default(), + ) + }); + + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); + + let subject_holder_validation = std::iter::once_with(|| { + options + .subject_holder_relationship + .as_ref() + .map(|(holder, relationship)| { + JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship) + }) + .unwrap_or(Ok(())) + }); + + let validation_units_iter = issuance_date_validation + .chain(expiry_date_validation) + .chain(structure_validation) + .chain(subject_holder_validation); + + let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err()); + let validation_errors: Vec = match fail_fast { + FailFast::FirstError => validation_units_error_iter.take(1).collect(), + FailFast::AllErrors => validation_units_error_iter.collect(), + }; + + if validation_errors.is_empty() { + Ok(()) + } else { + Err(CompoundCredentialValidationError { validation_errors }) + } + } + + /// Proof verification function + fn verify_proof( + credential: &Jpt, + issuer: &DOC, + options: &JwpVerificationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + + // If no method_url is set, parse the `kid` to a DID Url which should be the identifier + // of a verification method in a trusted issuer's DID document. + let method_id: DIDUrl = match &options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded + .get_header() + .kid() + .ok_or(JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract kid from protected header", + signer_ctx: SignerContext::Issuer, + })?; + + // Convert kid to DIDUrl + DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { + source: Some(err.into()), + message: "could not parse kid as a DID Url", + signer_ctx: SignerContext::Issuer, + })? + } + }; + + // check issuer + let issuer: &CoreDocument = issuer.as_ref(); + + if issuer.id() != method_id.did() { + return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer)); + } + + // Obtain the public key from the issuer's DID document + let public_key: JwkExt = issuer + .resolve_method(&method_id, options.method_scope) + .and_then(|method| method.data().public_key_jwk()) + .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type + .ok_or_else(|| JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract JWK from a method identified by kid", + signer_ctx: SignerContext::Issuer, + })?; + + let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?; + + // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before + // returning. + let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?; + if &issuer_id != method_id.did() { + return Err(JwtValidationError::IdentifierMismatch { + signer_ctx: SignerContext::Issuer, + }); + }; + Ok(credential_token) + } + + /// Verify the decoded issued JWP proof using the given `public_key`. + fn verify_decoded_jwp( + decoded: JwpIssuedDecoder, + public_key: &JwkExt, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Verify Jwp proof + let decoded_jwp = decoded + .verify(public_key) + .map_err(JwtValidationError::JwpProofVerificationError)?; + + let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded_jwp.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let custom_claims = credential_claims.custom.clone(); + + // Construct the credential token containing the credential and the protected header. + let credential: Credential = credential_claims + .try_into_credential() + .map_err(JwtValidationError::CredentialStructure)?; + + Ok(DecodedJptCredential { + credential, + custom_claims, + decoded_jwp, + }) + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs new file mode 100644 index 0000000000..258df619d4 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -0,0 +1,242 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use crate::credential::Credential; +use crate::revocation::RevocationDocumentExt; +use crate::revocation::RevocationTimeframeStatus; +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::DID; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwp::issued::JwpIssuedDecoder; + +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +/// Utility functions for verifying JPT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JptCredentialValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JptCredentialValidatorUtils { + /// Utility for extracting the issuer field of a [`Credential`] as a DID. + /// + /// # Errors + /// + /// Fails if the issuer field is not a valid DID. + pub fn extract_issuer(credential: &Credential) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// + /// # Errors + /// + /// If the JPT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_issued_jpt(credential: &Jpt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + let claims = decoded + .get_header() + .claims() + .ok_or("Claims not present") + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &Credential, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; + + Self::check_validity_timeframe(status, validity_timeframe) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } + + pub(crate) fn check_validity_timeframe( + status: RevocationTimeframeStatus, + validity_timeframe: Option, + ) -> ValidationUnitResult { + let timeframe = validity_timeframe.unwrap_or(Timestamp::now_utc()); + + let check = timeframe >= status.start_validity_timeframe() && timeframe <= status.end_validity_timeframe(); + + if !check { + Err(JwtValidationError::OutsideTimeframe) + } else { + Ok(()) + } + } + + /// Checks whether the credential status has been revoked. + /// + /// Only supports `RevocationTimeframe2024`. + pub fn check_revocation_with_validity_timeframe_2024< + DOC: AsRef + ?Sized, + T, + >( + credential: &Credential, + issuer: &DOC, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; + + Self::check_revocation_bitmap(issuer, status) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } + + /// Check the given `status` against the matching [`RevocationBitmap`] service in the issuer's DID Document. + fn check_revocation_bitmap + ?Sized>( + issuer: &DOC, + status: RevocationTimeframeStatus, + ) -> ValidationUnitResult { + let issuer_service_url: identity_did::DIDUrl = + identity_did::DIDUrl::parse(status.id().to_string()).map_err(|err| { + JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "could not convert status id to DIDUrl; {}", + err, + ))) + })?; + + // Check whether index is revoked. + let revocation_bitmap: crate::revocation::RevocationBitmap = issuer + .as_ref() + .resolve_revocation_bitmap(issuer_service_url.into()) + .map_err(|_| JwtValidationError::ServiceLookupError)?; + + if let Some(index) = status.index() { + if revocation_bitmap.is_revoked(index) { + return Err(JwtValidationError::Revoked); + } + } + Ok(()) + } + + /// Checks whether the credential status has been revoked or the timeframe interval is INVALID + /// + /// Only supports `RevocationTimeframe2024`. + pub fn check_timeframes_and_revocation_with_validity_timeframe_2024< + DOC: AsRef + ?Sized, + T, + >( + credential: &Credential, + issuer: &DOC, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; + + let revocation = std::iter::once_with(|| Self::check_revocation_bitmap(issuer, status.clone())); + + let timeframes = std::iter::once_with(|| Self::check_validity_timeframe(status.clone(), validity_timeframe)); + + let checks_iter = revocation.chain(timeframes); + + let checks_error_iter = checks_iter.filter_map(|result| result.err()); + + let mut checks_errors: Vec = checks_error_iter.take(1).collect(); + + match checks_errors.pop() { + Some(err) => Err(err), + None => Ok(()), + } + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/mod.rs b/identity_credential/src/validator/jpt_credential_validation/mod.rs new file mode 100644 index 0000000000..60455ba606 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_credential; +mod jpt_credential_validation_options; +mod jpt_credential_validator; +mod jpt_credential_validator_utils; + +pub use decoded_jpt_credential::*; +pub use jpt_credential_validation_options::*; +pub use jpt_credential_validator::*; +pub use jpt_credential_validator_utils::*; diff --git a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs new file mode 100644 index 0000000000..fb62181057 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs @@ -0,0 +1,22 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::common::Url; +use jsonprooftoken::jwp::presented::JwpPresented; + +use crate::credential::Credential; + +/// Decoded [`Credential`] from a cryptographically verified JWP. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJptPresentation { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub credential: Credential, + /// The `aud` property parsed from the JWT claims. + pub aud: Option, + /// The custom claims parsed from the JPT. + pub custom_claims: Option, + /// The decoded and verifier Issued JWP, will be used to construct the Presented JWP + pub decoded_jwp: JwpPresented, +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs new file mode 100644 index 0000000000..302b45f8c4 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_document::verifiable::JwpVerificationOptions; +use serde::Deserialize; +use serde::Serialize; + +/// Criteria for validating a [`Presentation`](crate::presentation::Presentation). +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct JptPresentationValidationOptions { + /// The nonce to be placed in the Presentation Protected Header. + #[serde(default)] + pub nonce: Option, + + /// Options which affect the verification of the proof on the credential. + #[serde(default)] + pub verification_options: JwpVerificationOptions, +} + +impl JptPresentationValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Declare that the presentation is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn nonce(mut self, nonce: impl Into) -> Self { + self.nonce = Some(nonce.into()); + self + } + + /// Set options which affect the verification of the JWP proof. + pub fn verification_options(mut self, options: JwpVerificationOptions) -> Self { + self.verification_options = options; + self + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs new file mode 100644 index 0000000000..ac32e9878f --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -0,0 +1,226 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use identity_core::common::Url; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwp::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::CompoundCredentialValidationError; +use crate::validator::FailFast; +use crate::validator::JwtCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +use super::DecodedJptPresentation; +use super::JptPresentationValidationOptions; + +/// A type for decoding and validating Presented [`Credential`]s in JPT format. +#[non_exhaustive] +pub struct JptPresentationValidator; + +impl JptPresentationValidator { + /// Decodes and validates a Presented [`Credential`] issued as a JPT (JWP Presented Form). A + /// [`DecodedJptPresentation`] is returned upon success. + /// + /// The following properties are validated according to `options`: + /// - the holder's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + pub fn validate( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + // First verify the JWP proof and decode the result into a presented credential token, then apply all other + // validations. + let presented_credential_token = + Self::verify_proof(presentation_jpt, issuer, options).map_err(|err| CompoundCredentialValidationError { + validation_errors: [err].into(), + })?; + + let credential: &Credential = &presented_credential_token.credential; + + Self::validate_presented_credential::(credential, fail_fast)?; + + Ok(presented_credential_token) + } + + pub(crate) fn validate_presented_credential( + credential: &Credential, + fail_fast: FailFast, + ) -> Result<(), CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); + + let validation_units_iter = structure_validation; + + let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err()); + let validation_errors: Vec = match fail_fast { + FailFast::FirstError => validation_units_error_iter.take(1).collect(), + FailFast::AllErrors => validation_units_error_iter.collect(), + }; + + if validation_errors.is_empty() { + Ok(()) + } else { + Err(CompoundCredentialValidationError { validation_errors }) + } + } + + /// Proof verification function + fn verify_proof( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let decoded: JwpPresentedDecoder = + JwpPresentedDecoder::decode(presentation_jpt.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + + let nonce: Option<&String> = options.nonce.as_ref(); + // Validate the nonce + if decoded.get_presentation_header().nonce() != nonce { + return Err(JwtValidationError::JwsDecodingError( + identity_verification::jose::error::Error::InvalidParam("invalid nonce value"), + )); + } + + // If no method_url is set, parse the `kid` to a DID Url which should be the identifier + // of a verification method in a trusted issuer's DID document. + let method_id: DIDUrl = match &options.verification_options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded + .get_issuer_header() + .kid() + .ok_or(JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract kid from protected header", + signer_ctx: SignerContext::Issuer, + })?; + + // Convert kid to DIDUrl + DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { + source: Some(err.into()), + message: "could not parse kid as a DID Url", + signer_ctx: SignerContext::Issuer, + })? + } + }; + + // check issuer + let issuer: &CoreDocument = issuer.as_ref(); + + if issuer.id() != method_id.did() { + return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer)); + } + + // Obtain the public key from the issuer's DID document + let public_key: JwkExt = issuer + .resolve_method(&method_id, options.verification_options.method_scope) + .and_then(|method| method.data().public_key_jwk()) + .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type + .ok_or_else(|| JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract JWK from a method identified by kid", + signer_ctx: SignerContext::Issuer, + })?; + + let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?; + + // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before + // returning. + let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?; + if &issuer_id != method_id.did() { + return Err(JwtValidationError::IdentifierMismatch { + signer_ctx: SignerContext::Issuer, + }); + }; + Ok(credential_token) + } + + /// Verify the decoded presented JWP proof using the given `public_key`. + fn verify_decoded_jwp( + decoded: JwpPresentedDecoder, + public_key: &JwkExt, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Verify Jwp proof + let decoded_jwp = decoded + .verify(public_key) + .map_err(JwtValidationError::JwpProofVerificationError)?; + + let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded_jwp.get_payloads(); + let mut jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + // if not set the deserializatioon will throw an error since even the iat is not set, so we set this to 0 + jpt_claims.nbf.map_or_else( + || { + jpt_claims.set_nbf(0); + }, + |_| (), + ); + + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let custom_claims = credential_claims.custom.clone(); + + // Construct the credential token containing the credential and the protected header. + let credential: Credential = credential_claims + .try_into_credential() + .map_err(JwtValidationError::CredentialStructure)?; + + let aud: Option = decoded_jwp.get_presentation_protected_header().aud().and_then(|aud| { + Url::from_str(aud) + .map_err(|_| { + JwtValidationError::JwsDecodingError(identity_verification::jose::error::Error::InvalidParam( + "invalid audience value", + )) + }) + .ok() + }); + + Ok(DecodedJptPresentation { + credential, + aud, + custom_claims, + decoded_jwp, + }) + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs new file mode 100644 index 0000000000..3bdf17a00e --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs @@ -0,0 +1,99 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::DID; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwp::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::revocation::RevocationTimeframeStatus; +use crate::revocation::VerifierRevocationTimeframeStatus; +use crate::validator::JptCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +/// Utility functions for verifying JPT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JptPresentationValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JptPresentationValidatorUtils { + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// + /// # Errors + /// + /// If the JPT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_presented_jpt(presentation: &Jpt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let decoded = JwpPresentedDecoder::decode(presentation.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + let claims = decoded + .get_issuer_header() + .claims() + .ok_or("Claims not present") + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &Credential, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: VerifierRevocationTimeframeStatus = + VerifierRevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + JptCredentialValidatorUtils::check_validity_timeframe(status.0, validity_timeframe) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/mod.rs b/identity_credential/src/validator/jpt_presentation_validation/mod.rs new file mode 100644 index 0000000000..1cab953dc5 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_presentation; +mod jpt_presentation_validation_options; +mod jpt_presentation_validator; +mod jpt_presentation_validator_utils; + +pub use decoded_jpt_presentation::*; +pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator::*; +pub use jpt_presentation_validator_utils::*; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index 073ffe303c..a531f088d7 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -104,6 +104,18 @@ pub enum JwtValidationError { /// Indicates that the credential has been suspended. #[error("credential has been suspended")] Suspended, + /// Indicates that the credential's timeframe interval is not valid + #[cfg(feature = "jpt-bbs-plus")] + #[error("timeframe interval not valid")] + OutsideTimeframe, + /// Indicates that the JWP representation of an issued credential or presentation could not be decoded. + #[cfg(feature = "jpt-bbs-plus")] + #[error("could not decode jwp")] + JwpDecodingError(#[source] jsonprooftoken::errors::CustomError), + /// Indicates that the verification of the JWP has failed + #[cfg(feature = "jpt-bbs-plus")] + #[error("could not verify jwp")] + JwpProofVerificationError(#[source] jsonprooftoken::errors::CustomError), } /// Specifies whether an error is related to a credential issuer or the presentation holder. diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index 37611334c3..2266618ddd 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -3,6 +3,10 @@ //! Verifiable Credential and Presentation validators. +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jpt_credential_validation::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jpt_presentation_validation::*; pub use self::jwt_credential_validation::*; pub use self::jwt_presentation_validation::*; pub use self::options::FailFast; @@ -11,6 +15,10 @@ pub use self::options::SubjectHolderRelationship; #[cfg(feature = "sd-jwt")] pub use self::sd_jwt::*; +#[cfg(feature = "jpt-bbs-plus")] +mod jpt_credential_validation; +#[cfg(feature = "jpt-bbs-plus")] +mod jpt_presentation_validation; mod jwt_credential_validation; mod jwt_presentation_validation; mod options; diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 87fddd0fed..1b226f9585 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -770,7 +770,7 @@ impl CoreDocument { } /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. - // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains + // NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains // services whose ids are of the form #. pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> where diff --git a/identity_document/src/verifiable/jwp_verification_options.rs b/identity_document/src/verifiable/jwp_verification_options.rs new file mode 100644 index 0000000000..65667968ea --- /dev/null +++ b/identity_document/src/verifiable/jwp_verification_options.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_did::DIDUrl; +use identity_verification::MethodScope; + +/// Holds additional options for verifying a JWP +#[non_exhaustive] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct JwpVerificationOptions { + /// Verify the signing verification method relation matches this. + pub method_scope: Option, + /// The DID URl of the method, whose JWK should be used to verify the JWP. + /// If unset, the `kid` of the JWP is used as the DID Url. + pub method_id: Option, +} + +impl JwpVerificationOptions { + /// Creates a new [`JwpVerificationOptions`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the scope of the verification methods that may be used to verify the given JWP. + pub fn method_scope(mut self, value: MethodScope) -> Self { + self.method_scope = Some(value); + self + } + + /// The DID URl of the method, whose JWK should be used to verify the JWP. + pub fn method_id(mut self, value: DIDUrl) -> Self { + self.method_id = Some(value); + self + } +} diff --git a/identity_document/src/verifiable/mod.rs b/identity_document/src/verifiable/mod.rs index da91055ca1..6f0386d3fb 100644 --- a/identity_document/src/verifiable/mod.rs +++ b/identity_document/src/verifiable/mod.rs @@ -3,6 +3,8 @@ //! Additional functionality for DID assisted digital signatures. +pub use self::jwp_verification_options::JwpVerificationOptions; pub use self::jws_verification_options::JwsVerificationOptions; +mod jwp_verification_options; mod jws_verification_options; diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index a5a4e5e1f6..0183994b24 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -23,7 +23,7 @@ identity_verification = { version = "=1.2.0", path = "../identity_verification", [dev-dependencies] anyhow = "1.0.64" -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client"] } +iota-sdk = { version = "1.1.5", default-features = false, features = ["tls", "client"] } rand = "0.8.5" tokio = { version = "1.29.0", features = ["full"] } @@ -64,6 +64,9 @@ memstore = ["identity_storage/memstore"] # Enables selective disclosure features. sd-jwt = ["identity_credential/sd-jwt"] +# Enables zero knowledge selective disclosurable VCs +jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-plus"] + [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 24a20359eb..9ab2e53805 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -105,7 +105,22 @@ pub mod verification { pub mod storage { //! Storage traits. - pub use identity_storage::*; + /// KeyIdStorage types and functionalities. + pub mod key_id_storage { + pub use identity_storage::key_id_storage::*; + } + /// KeyStorage types and functionalities. + pub mod key_storage { + pub use identity_storage::key_storage::public_modules::*; + } + /// Storage types and functionalities. + #[allow(clippy::module_inception)] + pub mod storage { + pub use identity_storage::storage::*; + } + pub use identity_storage::key_id_storage::*; + pub use identity_storage::key_storage::*; + pub use identity_storage::storage::*; } #[cfg(feature = "sd-jwt")] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 4e55ae36f4..8e0e7070e2 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -19,7 +19,7 @@ identity_credential = { version = "=1.2.0", path = "../identity_credential", def identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } -iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } +iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } once_cell = { version = "1.18", default-features = false, features = ["std"] } diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 27bf003e75..014ee4de7c 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -14,6 +14,7 @@ description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } +json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } subtle = { version = "2.5", default-features = false } diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs new file mode 100644 index 0000000000..97b68bf678 --- /dev/null +++ b/identity_jose/src/jwk/curve/bls.rs @@ -0,0 +1,43 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use core::fmt::Display; +use core::fmt::Formatter; +use core::fmt::Result; + +/// Supported BLS Curves. +/// +/// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-bls-key-representations-05#name-curve-parameter-registratio) +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum BlsCurve { + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G1. + BLS12381G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G2. + BLS12381G2, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G1. + BLS48581G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G2. + BLS48581G2, +} + +impl BlsCurve { + /// Returns the name of the curve as a string slice. + pub const fn name(self) -> &'static str { + match self { + Self::BLS12381G1 => "BLS12381G1", + Self::BLS12381G2 => "BLS12381G2", + Self::BLS48581G1 => "BLS48581G1", + Self::BLS48581G2 => "BLS48581G2", + } + } +} + +impl Display for BlsCurve { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str(self.name()) + } +} diff --git a/identity_jose/src/jwk/curve/mod.rs b/identity_jose/src/jwk/curve/mod.rs index 38a1e3bba7..8e1627219f 100644 --- a/identity_jose/src/jwk/curve/mod.rs +++ b/identity_jose/src/jwk/curve/mod.rs @@ -1,10 +1,12 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod bls; mod ec; mod ecx; mod ed; +pub use self::bls::*; pub use self::ec::*; pub use self::ecx::*; pub use self::ed::*; diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs new file mode 100644 index 0000000000..39fc02fa93 --- /dev/null +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -0,0 +1,162 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::Jwk; +use super::JwkOperation; +use super::JwkParams; +use super::JwkParamsEc; +use super::JwkType; +use super::JwkUse; +use identity_core::common::Url; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jwk::alg_parameters::Algorithm; +use jsonprooftoken::jwk::alg_parameters::JwkAlgorithmParameters; +use jsonprooftoken::jwk::alg_parameters::JwkEllipticCurveKeyParameters; +use jsonprooftoken::jwk::curves::EllipticCurveTypes; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwk::key::KeyOps; +use jsonprooftoken::jwk::key::PKUse; +use jsonprooftoken::jwk::types::KeyType; +use std::str::FromStr; + +impl From for JwkOperation { + fn from(value: KeyOps) -> Self { + match value { + KeyOps::Sign => Self::Sign, + KeyOps::Verify => Self::Verify, + KeyOps::Encrypt => Self::Encrypt, + KeyOps::Decrypt => Self::Decrypt, + KeyOps::WrapKey => Self::WrapKey, + KeyOps::UnwrapKey => Self::UnwrapKey, + KeyOps::DeriveKey => Self::DeriveKey, + KeyOps::DeriveBits => Self::DeriveBits, + KeyOps::ProofGeneration => Self::ProofGeneration, + KeyOps::ProofVerification => Self::ProofVerification, + } + } +} + +impl From for KeyOps { + fn from(value: JwkOperation) -> Self { + match value { + JwkOperation::Sign => Self::Sign, + JwkOperation::Verify => Self::Verify, + JwkOperation::Encrypt => Self::Encrypt, + JwkOperation::Decrypt => Self::Decrypt, + JwkOperation::WrapKey => Self::WrapKey, + JwkOperation::UnwrapKey => Self::UnwrapKey, + JwkOperation::DeriveKey => Self::DeriveKey, + JwkOperation::DeriveBits => Self::DeriveBits, + JwkOperation::ProofGeneration => Self::ProofGeneration, + JwkOperation::ProofVerification => Self::ProofVerification, + } + } +} + +impl From for JwkUse { + fn from(value: PKUse) -> Self { + match value { + PKUse::Signature => Self::Signature, + PKUse::Encryption => Self::Encryption, + PKUse::Proof => Self::Proof, + } + } +} + +impl From for PKUse { + fn from(value: JwkUse) -> Self { + match value { + JwkUse::Signature => Self::Signature, + JwkUse::Encryption => Self::Encryption, + JwkUse::Proof => Self::Proof, + } + } +} + +impl From for JwkParamsEc { + fn from(value: JwkEllipticCurveKeyParameters) -> Self { + Self { + crv: value.crv.to_string(), + x: value.x, + y: value.y, + d: value.d, + } + } +} + +impl TryInto for &JwkParamsEc { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + Ok(JwkEllipticCurveKeyParameters { + kty: KeyType::EllipticCurve, + crv: EllipticCurveTypes::from_str(&self.crv).map_err(|_| Self::Error::KeyError("crv not supported!"))?, + x: self.x.clone(), + y: self.y.clone(), + d: self.d.clone(), + }) + } +} + +impl TryFrom for Jwk { + type Error = crate::error::Error; + + fn try_from(value: JwkExt) -> Result { + let x5u = match value.x5u { + Some(v) => Some(Url::from_str(&v).map_err(|_| Self::Error::InvalidClaim("x5u"))?), + None => None, + }; + + let (kty, params) = match value.key_params { + JwkAlgorithmParameters::EllipticCurve(p) => (JwkType::Ec, JwkParams::Ec(JwkParamsEc::from(p))), + _ => unreachable!(), + }; + + Ok(Self { + kty, + use_: value.pk_use.map(JwkUse::from), + key_ops: value + .key_ops + .map(|vec_key_ops| vec_key_ops.into_iter().map(JwkOperation::from).collect()), + alg: value.alg.map(|a| a.to_string()), + kid: value.kid, + x5u, + x5c: value.x5c, + x5t: value.x5t, + x5t_s256: None, + params, + }) + } +} + +impl TryInto for &Jwk { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + let params = match &self.params { + JwkParams::Ec(p) => JwkAlgorithmParameters::EllipticCurve(p.try_into()?), + _ => return Err(Self::Error::InvalidParam("Parameters not supported!")), + }; + + let alg = match &self.alg { + Some(a) => Some(Algorithm::Proof( + ProofAlgorithm::from_str(a).map_err(|_| Self::Error::KeyError("Invalid alg"))?, + )), + None => None, + }; + + Ok(JwkExt { + kid: self.kid.clone(), + pk_use: self.use_.map(|u| u.into()), + key_ops: self + .key_ops + .as_deref() + .and_then(|vec_key_ops| vec_key_ops.iter().map(|o| Some((*o).into())).collect()), + alg, + x5u: self.x5u.as_ref().map(|v| v.as_str().to_string()), + x5c: self.x5c.clone(), + x5t: self.x5t.clone(), + key_params: params, + }) + } +} diff --git a/identity_jose/src/jwk/key_operation.rs b/identity_jose/src/jwk/key_operation.rs index 8fda0b6a23..ac6b7b0ce8 100644 --- a/identity_jose/src/jwk/key_operation.rs +++ b/identity_jose/src/jwk/key_operation.rs @@ -27,6 +27,10 @@ pub enum JwkOperation { DeriveKey, /// Derive bits not to be used as a key. DeriveBits, + /// Compute proof + ProofGeneration, + /// Verify proof + ProofVerification, } impl JwkOperation { @@ -41,6 +45,8 @@ impl JwkOperation { Self::UnwrapKey => "unwrapKey", Self::DeriveKey => "deriveKey", Self::DeriveBits => "deriveBits", + Self::ProofGeneration => "proofGeneration", + Self::ProofVerification => "proofVerification", } } @@ -55,6 +61,8 @@ impl JwkOperation { Self::UnwrapKey => Self::WrapKey, Self::DeriveKey => Self::DeriveKey, Self::DeriveBits => Self::DeriveBits, + Self::ProofGeneration => Self::ProofVerification, + Self::ProofVerification => Self::ProofGeneration, } } } diff --git a/identity_jose/src/jwk/key_params.rs b/identity_jose/src/jwk/key_params.rs index b7e3af17a5..9d1437637a 100644 --- a/identity_jose/src/jwk/key_params.rs +++ b/identity_jose/src/jwk/key_params.rs @@ -10,6 +10,8 @@ use crate::jwk::EcxCurve; use crate::jwk::EdCurve; use crate::jwk::JwkType; +use super::BlsCurve; + /// Algorithm-specific parameters for JSON Web Keys. /// /// [More Info](https://tools.ietf.org/html/rfc7518#section-6) @@ -155,6 +157,17 @@ impl JwkParamsEc { _ => Err(Error::KeyError("Ec Curve")), } } + + /// Returns the [`BlsCurve`] if it is of a supported type. + pub fn try_bls_curve(&self) -> Result { + match &*self.crv { + "BLS12381G1" => Ok(BlsCurve::BLS12381G1), + "BLS12381G2" => Ok(BlsCurve::BLS12381G2), + "BLS48581G1" => Ok(BlsCurve::BLS48581G1), + "BLS48581G2" => Ok(BlsCurve::BLS48581G2), + _ => Err(Error::KeyError("BLS Curve")), + } + } } impl From for JwkParams { diff --git a/identity_jose/src/jwk/key_use.rs b/identity_jose/src/jwk/key_use.rs index a686ba79cc..edd427c578 100644 --- a/identity_jose/src/jwk/key_use.rs +++ b/identity_jose/src/jwk/key_use.rs @@ -16,6 +16,9 @@ pub enum JwkUse { /// Encryption. #[serde(rename = "enc")] Encryption, + /// Proof + #[serde(rename = "proof")] + Proof, } impl JwkUse { @@ -24,6 +27,7 @@ impl JwkUse { match self { Self::Signature => "sig", Self::Encryption => "enc", + Self::Proof => "proof", } } } diff --git a/identity_jose/src/jwk/mod.rs b/identity_jose/src/jwk/mod.rs index a714cbf5ac..780c7f9861 100644 --- a/identity_jose/src/jwk/mod.rs +++ b/identity_jose/src/jwk/mod.rs @@ -4,6 +4,7 @@ //! JSON Web Keys ([JWK](https://tools.ietf.org/html/rfc7517)) mod curve; +mod jwk_ext; mod key; mod key_operation; mod key_params; diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 26f4fd6290..d176fc5437 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -32,7 +32,7 @@ optional = true [dev-dependencies] identity_iota_core = { path = "../identity_iota_core", features = ["test"] } -iota-sdk = { version = "1.0.2" } +iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 1510ab0b21..590d532485 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -12,21 +12,24 @@ rust-version.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] +anyhow = "1.0.82" async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } +identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.2.0", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } +zkryptium = { workspace = true, optional = true } [dev-dependencies] identity_credential = { version = "=1.2.0", path = "../identity_credential", features = ["revocation-bitmap"] } @@ -42,6 +45,8 @@ memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] +# Enables JSON Proof Token & BBS+ related features +jpt-bbs-plus = ["identity_credential/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] [lints] workspace = true diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs new file mode 100644 index 0000000000..2a3b38a0a7 --- /dev/null +++ b/identity_storage/src/key_storage/bls.rs @@ -0,0 +1,203 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use identity_verification::jose::jwk::Jwk; +use identity_verification::jose::jwu; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::JwkParamsEc; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use zkryptium::bbsplus::ciphersuites::BbsCiphersuite; +use zkryptium::bbsplus::ciphersuites::Bls12381Sha256; +use zkryptium::bbsplus::ciphersuites::Bls12381Shake256; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::keys::pair::KeyPair; +use zkryptium::schemes::algorithms::BBSplus; +use zkryptium::schemes::generics::Signature; + +use crate::key_storage::KeyStorageError; +use crate::key_storage::KeyStorageErrorKind; +use crate::key_storage::KeyStorageResult; +use crate::ProofUpdateCtx; + +fn random_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + KeyPair::>::random().map(KeyPair::into_parts) +} + +/// Generates a new BBS+ keypair using either `BLS12381-SHA256` or `BLS12381-SHAKE256`. +pub fn generate_bbs_keypair(alg: ProofAlgorithm) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => random_bbs_keypair::(), + ProofAlgorithm::BLS12381_SHAKE256 => random_bbs_keypair::(), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)) +} + +/// Encodes a private BBS+ key into JWK. +pub fn encode_bls_jwk( + private_key: &BBSplusSecretKey, + public_key: &BBSplusPublicKey, + alg: ProofAlgorithm, +) -> (Jwk, Jwk) { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let params = JwkParamsEc { + x, + y, + d: Some(d), + crv: BlsCurve::BLS12381G2.name().to_owned(), + }; + + let mut jwk = Jwk::from_params(params); + + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let public_jwk = jwk.to_public().expect("kty != oct"); + + (jwk, public_jwk) +} + +/// Attempts to decode JWK into a BBS+ keypair. +pub fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(Option, BBSplusPublicKey)> { + // Check the provided JWK represents a BLS12381G2 key. + let params = jwk + .try_ec_params() + .ok() + .filter(|params| { + params + .try_bls_curve() + .map(|curve| curve == BlsCurve::BLS12381G2) + .unwrap_or(false) + }) + .context(format!("not a {} curve key", BlsCurve::BLS12381G2)) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + + let sk = params + .d + .as_deref() + .map(|d| { + jwu::decode_b64(d) + .context("`d` parameter is not base64 encoded") + .and_then(|bytes| BBSplusSecretKey::from_bytes(&bytes).context("invalid key size")) + }) + .transpose() + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + + let x = jwu::decode_b64(¶ms.x) + .context("`x` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + let y = jwu::decode_b64(¶ms.y) + .context("`y` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + + let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("invalid BBS+ public key".to_owned()) + })?; + + Ok((sk, pk)) +} + +fn _sign_bbs( + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> Result, zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + Signature::>::sign(Some(data), sk, pk, Some(header)).map(|s| s.to_bytes().to_vec()) +} + +/// Signs data and header using the given keys. +pub fn sign_bbs( + alg: ProofAlgorithm, + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> KeyStorageResult> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => _sign_bbs::(data, sk, pk, header), + ProofAlgorithm::BLS12381_SHAKE256 => _sign_bbs::(data, sk, pk, header), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("signature failed".to_owned()) + }) +} + +fn _update_bbs_signature( + sig: &[u8; 80], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> Result<[u8; 80], zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + let sig = Signature::>::from_bytes(sig)?; + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = update_ctx; + let half_updated = sig.update_signature( + sk, + old_start_validity_timeframe, + new_start_validity_timeframe, + *index_start_validity_timeframe, + *number_of_signed_messages, + )?; + half_updated + .update_signature( + sk, + old_end_validity_timeframe, + new_end_validity_timeframe, + *index_end_validity_timeframe, + *number_of_signed_messages, + ) + .map(|sig| sig.to_bytes()) +} + +/// Updates BBS+ signature's timeframe data. +pub fn update_bbs_signature( + alg: ProofAlgorithm, + sig: &[u8], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> KeyStorageResult> { + let exact_size_signature = sig.try_into().map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid signature size".to_owned()) + })?; + match alg { + ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(exact_size_signature, sk, update_ctx), + ProofAlgorithm::BLS12381_SHAKE256 => { + _update_bbs_signature::(exact_size_signature, sk, update_ctx) + } + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map(Vec::from) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature failed") + .with_source(e) + }) +} diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..276c39d4cb --- /dev/null +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_verification::jwk::Jwk; +use jsonprooftoken::jpa::algs::ProofAlgorithm; + +use crate::JwkGenOutput; +use crate::JwkStorage; +use crate::KeyId; +use crate::KeyStorageResult; +use crate::KeyType; +use crate::ProofUpdateCtx; + +/// Extension to the JwkStorage to handle BBS+ keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkStorageBbsPlusExt: JwkStorage { + /// Generates a JWK representing a BBS+ signature + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; + + /// Sign the provided `data` and `header` using the private key identified by `key_id` according to the requirements + /// of the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult>; + + /// Update proof functionality for timeframe revocation mechanism + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult>; +} diff --git a/identity_storage/src/key_storage/key_storage_error.rs b/identity_storage/src/key_storage/key_storage_error.rs index 59ff10968e..060d0794a6 100644 --- a/identity_storage/src/key_storage/key_storage_error.rs +++ b/identity_storage/src/key_storage/key_storage_error.rs @@ -23,6 +23,9 @@ pub enum KeyStorageErrorKind { /// Indicates an attempt to parse a signature algorithm that is not recognized by the key storage implementation. UnsupportedSignatureAlgorithm, + /// Indicates an attempt to parse a proof algorithm that is not recognized by the key storage implementation. + UnsupportedProofAlgorithm, + /// Indicates that the key storage implementation is not able to find the requested key. KeyNotFound, @@ -59,6 +62,7 @@ impl KeyStorageErrorKind { Self::UnsupportedKeyType => "key generation failed: the provided multikey schema is not supported", Self::KeyAlgorithmMismatch => "the key type cannot be used with the algorithm", Self::UnsupportedSignatureAlgorithm => "signing algorithm parsing failed", + Self::UnsupportedProofAlgorithm => "proof algorithm parsing failed", Self::KeyNotFound => "key not found in storage", Self::Unavailable => "key storage unavailable", Self::Unauthenticated => "authentication with the key storage failed", diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 08d1acb519..9bf4e6ea9a 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use core::fmt::Debug; @@ -12,6 +12,7 @@ use identity_verification::jose::jwk::EdCurve; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; +use identity_verification::jwk::BlsCurve; use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; @@ -66,6 +67,12 @@ impl JwkStorage for JwkMemStore { let public_key = private_key.public_key(); (private_key, public_key) } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } }; let kid: KeyId = random_key_id(); @@ -183,18 +190,24 @@ impl JwkStorage for JwkMemStore { #[derive(Debug, Copy, Clone)] enum MemStoreKeyType { Ed25519, + BLS12381G2, } impl JwkMemStore { const ED25519_KEY_TYPE_STR: &'static str = "Ed25519"; /// The Ed25519 key type. pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(Self::ED25519_KEY_TYPE_STR); + + const BLS12381G2_KEY_TYPE_STR: &'static str = "BLS12381G2"; + /// The BLS12381G2 key type + pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381G2_KEY_TYPE_STR); } impl MemStoreKeyType { const fn name(&self) -> &'static str { match self { - MemStoreKeyType::Ed25519 => "Ed25519", + MemStoreKeyType::Ed25519 => JwkMemStore::ED25519_KEY_TYPE_STR, + MemStoreKeyType::BLS12381G2 => JwkMemStore::BLS12381G2_KEY_TYPE_STR, } } } @@ -211,6 +224,7 @@ impl TryFrom<&KeyType> for MemStoreKeyType { fn try_from(value: &KeyType) -> Result { match value.as_str() { JwkMemStore::ED25519_KEY_TYPE_STR => Ok(MemStoreKeyType::Ed25519), + JwkMemStore::BLS12381G2_KEY_TYPE_STR => Ok(MemStoreKeyType::BLS12381G2), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -239,6 +253,24 @@ impl TryFrom<&Jwk> for MemStoreKeyType { ), } } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected EC parameters for a JWK with `kty` Ec") + .with_source(err) + })?; + match ec_params.try_bls_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + BlsCurve::BLS12381G2 => Ok(MemStoreKeyType::BLS12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } other => Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) .with_custom_message(format!("Jwk `kty` {other} not supported")), @@ -269,6 +301,125 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: JwsAlgorithm) -> } } +#[cfg(feature = "jpt-bbs-plus")] +mod bbs_plus_impl { + use std::str::FromStr as _; + + use crate::key_storage::bls::encode_bls_jwk; + use crate::key_storage::bls::expand_bls_jwk; + use crate::key_storage::bls::generate_bbs_keypair; + use crate::key_storage::bls::sign_bbs; + use crate::key_storage::bls::update_bbs_signature; + use crate::JwkGenOutput; + use crate::JwkMemStore; + use crate::JwkStorageBbsPlusExt; + use crate::KeyId; + use crate::KeyStorageError; + use crate::KeyStorageErrorKind; + use crate::KeyStorageResult; + use crate::KeyType; + use crate::ProofUpdateCtx; + use async_trait::async_trait; + use identity_verification::jwk::BlsCurve; + use identity_verification::jwk::Jwk; + use jsonprooftoken::jpa::algs::ProofAlgorithm; + + use super::random_key_id; + + /// JwkStorageBbsPlusExt implementation for JwkMemStore + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwkStorageBbsPlusExt for JwkMemStore { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + if key_type != JwkMemStore::BLS12381G2_KEY_TYPE { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("unsupported key type {key_type}")), + ); + } + + let (private_key, public_key) = generate_bbs_keypair(alg)?; + let (jwk, public_jwk) = encode_bls_jwk(&private_key, &public_key, alg); + + let kid: KeyId = random_key_id(); + let mut jwk_store = self.jwk_store.write().await; + jwk_store.insert(kid.clone(), jwk); + + Ok(JwkGenOutput::new(kid, public_jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let jwk_store = self.jwk_store.read().await; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).ok()) + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm)?; + + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); + } + + // Obtain the corresponding private key. + let jwk: &Jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; + let (sk, pk) = expand_bls_jwk(jwk)?; + + sign_bbs(alg, data, &sk.expect("jwk is private"), &pk, header) + } + + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult> { + let jwk_store = self.jwk_store.read().await; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); + } + + // Obtain the corresponding private key. + let jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; + let sk = expand_bls_jwk(jwk)?.0.expect("jwk is private"); + + // Update the signature. + update_bbs_signature(alg, signature, &sk, &ctx) + } + } +} pub(crate) mod shared { use core::fmt::Debug; use core::fmt::Formatter; diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 70006b0537..f54f9d5233 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,10 +6,15 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. +#[cfg(feature = "jpt-bbs-plus")] +/// BLS12381 utils. +pub mod bls; #[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; mod jwk_storage; +#[cfg(feature = "jpt-bbs-plus")] +mod jwk_storage_bbs_plus_ext; mod key_id; mod key_storage_error; mod key_type; @@ -19,10 +24,17 @@ mod memstore; #[cfg(test)] pub(crate) mod tests; -pub use jwk_gen_output::*; -pub use jwk_storage::*; -pub use key_id::*; -pub use key_storage_error::*; -pub use key_type::*; -#[cfg(feature = "memstore")] -pub use memstore::*; +/// All modules that should be made available to end-users. +pub mod public_modules { + pub use super::jwk_gen_output::*; + pub use super::jwk_storage::*; + #[cfg(feature = "jpt-bbs-plus")] + pub use super::jwk_storage_bbs_plus_ext::*; + pub use super::key_id::*; + pub use super::key_storage_error::*; + pub use super::key_type::*; + #[cfg(feature = "memstore")] + pub use super::memstore::*; +} + +pub use public_modules::*; diff --git a/identity_storage/src/lib.rs b/identity_storage/src/lib.rs index da1b0b66f4..643e1e7444 100644 --- a/identity_storage/src/lib.rs +++ b/identity_storage/src/lib.rs @@ -19,5 +19,5 @@ pub mod key_storage; pub mod storage; pub use key_id_storage::*; -pub use key_storage::*; +pub use key_storage::public_modules::*; pub use storage::*; diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 7abac68286..a5d8d11185 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -27,6 +27,16 @@ pub enum JwkStorageDocumentError { /// Caused by an invalid JWS algorithm. #[error("invalid JWS algorithm")] InvalidJwsAlgorithm, + /// Caused by an invalid JWP algorithm. + #[error("invalid JWP algorithm")] + InvalidJwpAlgorithm, + /// Cannot cunstruct a valid Jwp (issued or presented form) + #[error("Not able to construct a valid Jwp")] + JwpBuildingError, + /// Credential's proof update internal error + #[error("Credential's proof internal error")] + ProofUpdateError(String), + /// Caused by a failure to construct a verification method. #[error("method generation failed: unable to create a valid verification method")] VerificationMethodConstructionError(#[source] identity_verification::Error), diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 8b412a285a..f9ee100986 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -153,20 +153,20 @@ mod private { // copious amounts of repetition. // NOTE: If such use of macros becomes very common it is probably better to use the duplicate crate: https://docs.rs/duplicate/latest/duplicate/ macro_rules! generate_method_for_document_type { - ($t:ty, $name:ident) => { + ($t:ty, $a:ty, $k:path, $f:path, $name:ident) => { async fn $name( document: &mut $t, storage: &Storage, key_type: KeyType, - alg: JwsAlgorithm, + alg: $a, fragment: Option<&str>, scope: MethodScope, ) -> StorageResult where - K: JwkStorage, + K: $k, I: KeyIdStorage, { - let JwkGenOutput { key_id, jwk } = ::generate(&storage.key_storage(), key_type, alg) + let JwkGenOutput { key_id, jwk } = $f(storage.key_storage(), key_type, alg) .await .map_err(Error::KeyStorageError)?; @@ -304,7 +304,13 @@ macro_rules! purge_method_for_document_type { // CoreDocument // ==================================================================================================================== -generate_method_for_document_type!(CoreDocument, generate_method_core_document); +generate_method_for_document_type!( + CoreDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_core_document +); purge_method_for_document_type!(CoreDocument, purge_method_core_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] @@ -505,7 +511,7 @@ impl JwkDocumentExt for CoreDocument { /// Attempt to revert key generation. If this succeeds the original `source_error` is returned, /// otherwise [`JwkStorageDocumentError::UndoOperationFailed`] is returned with the `source_error` attached as /// `source`. -async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error +pub(crate) async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error where K: JwkStorage, I: KeyIdStorage, @@ -531,7 +537,13 @@ mod iota_document { use identity_credential::credential::Jwt; use identity_iota_core::IotaDocument; - generate_method_for_document_type!(IotaDocument, generate_method_iota_document); + generate_method_for_document_type!( + IotaDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_iota_document + ); purge_method_for_document_type!(IotaDocument, purge_method_iota_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs new file mode 100644 index 0000000000..21ef7fafaa --- /dev/null +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -0,0 +1,362 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::key_id_storage::MethodDigest; +use crate::try_undo_key_generation; +use crate::JwkGenOutput; +use crate::JwkStorageBbsPlusExt; +use crate::KeyIdStorage; +use crate::KeyType; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Object; +use identity_core::convert::ToJson; +use identity_credential::credential::Credential; +use identity_credential::credential::Jpt; +use identity_credential::credential::JwpCredentialOptions; +use identity_credential::presentation::JwpPresentationOptions; +use identity_credential::presentation::SelectiveDisclosurePresentation; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssuedBuilder; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Handle JWP-based operations on DID Documents. +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwpDocumentExt { + /// Generate new key material in the given `storage` and insert a new verification method with the corresponding + /// public key material into the DID document. This supports BBS+ keys. + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; + + /// Compute a JWP in the Issued form representing the Verifiable Credential + /// See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-issued-form) + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; + + /// Compute a JWP in the Presented form representing the presented Verifiable Credential after the Selective + /// Disclosure of attributes See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form) + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; + + /// Produces a JPT where the payload is produced from the given `credential`. + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; + + /// Produces a JPT where the payload contains the Selective Disclosed attributes of a `credential`. + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +generate_method_for_document_type!( + CoreDocument, + ProofAlgorithm, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, + generate_method_core_document +); + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwpDocumentExt for CoreDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + generate_method_core_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let typ = "JPT".to_string(); + + let kid = if let Some(ref kid) = options.kid { + kid.clone() + } else { + method.id().to_string() + }; + + let mut issuer_header = IssuerProtectedHeader::new(alg); + issuer_header.set_typ(Some(typ)); + issuer_header.set_kid(Some(kid)); + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let jwp_builder = JwpIssuedBuilder::new(issuer_header, jpt_claims.clone()); + + let header = jwp_builder.get_issuer_protected_header().map_or_else( + || Err(Error::JwpBuildingError), + |h| h.to_json_vec().map_err(|_| Error::JwpBuildingError), + )?; + + let data = jwp_builder.get_payloads().map_or_else( + || Err(Error::JwpBuildingError), + |p| p.to_bytes().map_err(|_| Error::JwpBuildingError), + )?; + + let signature = ::sign_bbs(storage.key_storage(), &key_id, &data, &header, jwk) + .await + .map_err(Error::KeyStorageError)?; + + jwp_builder + .build_with_proof(signature) + .map_err(|_| Error::JwpBuildingError)? + .encode(SerializationType::COMPACT) + .map_err(|err| Error::EncodingError(Box::new(err))) + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(method_id, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let public_key: Jwk = jwk.try_into().map_err(|_| Error::NotPublicKeyJwk)?; + + let mut presentation_header = PresentationProtectedHeader::new(alg.into()); + presentation_header.set_nonce(options.nonce.clone()); + presentation_header.set_aud(options.audience.as_ref().map(|u| u.to_string())); + + presentation.set_presentation_header(presentation_header); + + let jwp_builder = presentation.builder(); + + let presented_jwp = jwp_builder.build(&public_key).map_err(|_| Error::JwpBuildingError)?; + + Ok( + presented_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?, + ) + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + let jpt_claims = credential + .serialize_jpt(custom_claims) + .map_err(Error::ClaimsSerializationError)?; + + self + .create_issued_jwp(storage, fragment, &jpt_claims, options) + .await + .map(Jpt::new) + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .create_presented_jwp(presentation, method_id, options) + .await + .map(Jpt::new) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + generate_method_for_document_type!( + IotaDocument, + ProofAlgorithm, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, + generate_method_iota_document + ); + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwpDocumentExt for IotaDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + self + .core_document() + .create_issued_jwp(storage, fragment, jpt_claims, options) + .await + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presented_jwp(presentation, method_id, options) + .await + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .create_credential_jpt(credential, storage, fragment, options, custom_claims) + .await + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presentation_jpt(presentation, method_id, options) + .await + } + } +} diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index efbdc28cbb..7643c41a95 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -4,14 +4,25 @@ //! This module provides a type wrapping a key and key id storage. mod error; +#[macro_use] mod jwk_document_ext; +#[cfg(feature = "jpt-bbs-plus")] +mod jwp_document_ext; mod signature_options; +#[cfg(feature = "jpt-bbs-plus")] +mod timeframe_revocation_ext; + #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; pub use error::*; + pub use jwk_document_ext::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use jwp_document_ext::*; pub use signature_options::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use timeframe_revocation_ext::*; /// A type wrapping a key and key id storage, typically used with [`JwkStorage`](crate::key_storage::JwkStorage) and /// [`KeyIdStorage`](crate::key_id_storage::KeyIdStorage) that should always be used together when calling methods from diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs new file mode 100644 index 0000000000..f53f2a9639 --- /dev/null +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -0,0 +1,198 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::JwkStorageBbsPlusExt; +use crate::KeyIdStorage; +use crate::MethodDigest; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Duration; +use identity_core::common::Timestamp; +use identity_credential::credential::Jpt; +use identity_credential::revocation::RevocationTimeframeStatus; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::payloads::Payloads; +use jsonprooftoken::jwp::issued::JwpIssued; +use serde_json::Value; +use zkryptium::bbsplus::signature::BBSplusSignature; + +/// Contains information needed to update the signature in the RevocationTimeframe2024 revocation mechanism. +pub struct ProofUpdateCtx { + /// Old `startValidityTimeframe` value + pub old_start_validity_timeframe: Vec, + /// New `startValidityTimeframe` value to be signed + pub new_start_validity_timeframe: Vec, + /// Old `endValidityTimeframe` value + pub old_end_validity_timeframe: Vec, + /// New `endValidityTimeframe` value to be signed + pub new_end_validity_timeframe: Vec, + /// Index of `startValidityTimeframe` claim inside the array of Claims + pub index_start_validity_timeframe: usize, + /// Index of `endValidityTimeframe` claim inside the array of Claims + pub index_end_validity_timeframe: usize, + /// Number of signed messages, number of payloads in a JWP + pub number_of_signed_messages: usize, +} + +/// CoreDocument and IotaDocument extension to handle Credential' signature update for RevocationTimeframe2024 +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait TimeframeRevocationExtension { + /// Update Credential' signature considering the Timeframe interval + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl TimeframeRevocationExtension for CoreDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let new_start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); + let new_end_validity_timeframe = new_start_validity_timeframe + .checked_add(duration) + .ok_or(Error::ProofUpdateError("Invalid granularity".to_owned()))?; + let new_start_validity_timeframe = new_start_validity_timeframe.to_rfc3339(); + let new_end_validity_timeframe = new_end_validity_timeframe.to_rfc3339(); + + let proof = credential_jwp.get_proof(); + let claims = credential_jwp + .get_claims() + .ok_or(Error::ProofUpdateError("Should not happen".to_owned()))?; + let mut payloads: Payloads = credential_jwp.get_payloads().clone(); + + let index_start_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'startValidityTimeframe' property NOT found".to_owned(), + ))?; + let index_end_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'endValidityTimeframe' property NOT found".to_owned(), + ))?; + + let old_start_validity_timeframe = payloads + .replace_payload_at_index( + index_start_validity_timeframe, + Value::String(new_start_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let old_end_validity_timeframe = payloads + .replace_payload_at_index( + index_end_validity_timeframe, + Value::String(new_end_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let proof: [u8; BBSplusSignature::BYTES] = proof + .try_into() + .map_err(|_| Error::ProofUpdateError("Invalid bytes length of JWP proof".to_owned()))?; + + let proof_update_ctx = ProofUpdateCtx { + old_start_validity_timeframe: serde_json::to_vec(&old_start_validity_timeframe).unwrap(), + new_start_validity_timeframe: serde_json::to_vec(&new_start_validity_timeframe).unwrap(), + old_end_validity_timeframe: serde_json::to_vec(&old_end_validity_timeframe).unwrap(), + new_end_validity_timeframe: serde_json::to_vec(&new_end_validity_timeframe).unwrap(), + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages: payloads.0.len(), + }; + + let new_proof = + ::update_signature(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) + .await + .map_err(Error::KeyStorageError)?; + + credential_jwp.set_proof(&new_proof); + credential_jwp.set_payloads(payloads); + + let jpt = credential_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?; + + Ok(Jpt::new(jpt)) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl TimeframeRevocationExtension for IotaDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + self + .core_document() + .update(storage, fragment, start_validity, duration, credential_jwp) + .await + } + } +} diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index fd795bfd99..40f549e5d2 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -16,20 +16,27 @@ async-trait = { version = "0.1.64", default-features = false } identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } -iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } -iota_stronghold = { version = "2.0", default-features = false } +iota-sdk = { version = "1.1.5", default-features = false, features = ["client", "stronghold"] } +iota_stronghold = { version = "2.1.0", default-features = false } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } zeroize = { version = "1.6.0", default_features = false } +zkryptium = { workspace = true, optional = true } [dev-dependencies] +anyhow = "1.0.82" identity_did = { version = "=1.2.0", path = "../identity_did", default_features = false } +identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false, features = ["jpt-bbs-plus"] } +json-proof-token = { workspace = true } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } +zkryptium = { workspace = true } [features] default = [] # Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. send-sync-storage = ["identity_storage/send-sync-storage"] +bbs-plus = ["identity_storage/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] [lints] workspace = true diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index decb2c4c00..ae8f8aef5b 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod ed25519; -mod stronghold_jwk_storage; -mod stronghold_key_id; +mod storage; +pub(crate) mod stronghold_key_type; #[cfg(test)] mod tests; +pub(crate) mod utils; -pub use stronghold_jwk_storage::*; +pub use storage::*; +pub use stronghold_key_type::*; diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs new file mode 100644 index 0000000000..cb02b9274b --- /dev/null +++ b/identity_stronghold/src/storage/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod stronghold_jwk_storage; +#[cfg(any(feature = "bbs-plus", test))] +mod stronghold_jwk_storage_bbs_plus_ext; +mod stronghold_key_id; + +use std::sync::Arc; + +#[cfg(feature = "bbs-plus")] +use identity_storage::key_storage::bls::encode_bls_jwk; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsOkp; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jwu; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::KeyType as ProceduresKeyType; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::procedures::StrongholdProcedure; +use iota_stronghold::Location; +use iota_stronghold::Stronghold; +#[cfg(feature = "bbs-plus")] +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use tokio::sync::MutexGuard; +#[cfg(feature = "bbs-plus")] +use zeroize::Zeroizing; +#[cfg(feature = "bbs-plus")] +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; + +/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) +/// and [`JwkStorage`](crate::JwkStorage) interfaces. +#[derive(Clone, Debug)] +pub struct StrongholdStorage(Arc); + +impl StrongholdStorage { + /// Creates a new [`StrongholdStorage`]. + pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { + Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) + } + + /// Shared reference to the inner [`SecretManager`]. + pub fn as_secret_manager(&self) -> &SecretManager { + self.0.as_ref() + } + + /// Acquire lock of the inner [`Stronghold`]. + pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { + match *self.0 { + SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, + _ => unreachable!("secret manager can be only constructed from stronghold"), + } + } + + async fn get_ed25519_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + let public_key_procedure = iota_stronghold::procedures::PublicKey { + ty: ProceduresKeyType::Ed25519, + private_key: location, + }; + + let procedure_result = client + .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(err))?; + + let public_key: Vec = procedure_result.into(); + + let mut params = JwkParamsOkp::new(); + params.x = jwu::encode_b64(public_key); + EdCurve::Ed25519.name().clone_into(&mut params.crv); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } + + #[cfg(feature = "bbs-plus")] + async fn get_bls12381g2_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + client + .get_guards([location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let pk = sk.public_key(); + let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BLS12381_SHA256).1; + + drop(Zeroizing::new(sk.to_bytes())); + Ok(public_jwk) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(e)) + } + + /// Attepts to retrieve the public key corresponding to the key of id `key_id`, + /// returning it as a `key_type` encoded public JWK. + pub async fn get_public_key_with_type(&self, key_id: &KeyId, key_type: StrongholdKeyType) -> KeyStorageResult { + match key_type { + StrongholdKeyType::Ed25519 => self.get_ed25519_public_key(key_id).await, + #[cfg(feature = "bbs-plus")] + StrongholdKeyType::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, + #[allow(unreachable_patterns)] + _ => Err(KeyStorageErrorKind::UnsupportedKeyType.into()), + } + } + + /// Retrieve the public key corresponding to `key_id`. + #[deprecated(since = "1.3.0", note = "use `get_public_key_with_type` instead")] + pub async fn get_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + let public_key_procedure = iota_stronghold::procedures::PublicKey { + ty: ProceduresKeyType::Ed25519, + private_key: location, + }; + + let procedure_result = client + .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(err))?; + + let public_key: Vec = procedure_result.into(); + + let mut params = JwkParamsOkp::new(); + params.x = jwu::encode_b64(public_key); + EdCurve::Ed25519.name().clone_into(&mut params.crv); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } +} diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs similarity index 50% rename from identity_stronghold/src/stronghold_jwk_storage.rs rename to identity_stronghold/src/storage/stronghold_jwk_storage.rs index f3335883b5..b0400c8f65 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -14,89 +14,19 @@ use identity_storage::KeyType; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkParamsOkp; -use identity_verification::jwk::JwkType; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; use iota_stronghold::procedures::Ed25519Sign; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; use iota_stronghold::procedures::StrongholdProcedure; -use iota_stronghold::Client; -use iota_stronghold::ClientError; use iota_stronghold::Location; -use iota_stronghold::Stronghold; -use rand::distributions::DistString; -use std::fmt::Display; use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::MutexGuard; use crate::ed25519; - -const ED25519_KEY_TYPE_STR: &str = "Ed25519"; -static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; -pub(crate) static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; - -/// The Ed25519 key type. -pub const ED25519_KEY_TYPE: &KeyType = &KeyType::from_static_str(ED25519_KEY_TYPE_STR); - -/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) -/// and [`JwkStorage`](crate::JwkStorage) interfaces. -#[derive(Clone, Debug)] -pub struct StrongholdStorage(Arc); - -impl StrongholdStorage { - /// Creates a new [`StrongholdStorage`]. - pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { - Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) - } - - /// Shared reference to the inner [`SecretManager`]. - pub fn as_secret_manager(&self) -> &SecretManager { - self.0.as_ref() - } - - /// Acquire lock of the inner [`Stronghold`]. - pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { - match *self.0 { - SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, - _ => unreachable!("secret manager can be only constructed from stronghold"), - } - } - - /// Retrieve the public key corresponding to `key_id`. - pub async fn get_public_key(&self, key_id: &KeyId) -> KeyStorageResult { - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - - let location = Location::generic( - IDENTITY_VAULT_PATH.as_bytes().to_vec(), - key_id.to_string().as_bytes().to_vec(), - ); - - let public_key_procedure = iota_stronghold::procedures::PublicKey { - ty: ProceduresKeyType::Ed25519, - private_key: location, - }; - - let procedure_result = client - .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(err))?; - - let public_key: Vec = procedure_result.into(); - - let mut params = JwkParamsOkp::new(); - params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_string(); - let mut jwk: Jwk = Jwk::from_params(params); - jwk.set_alg(JwsAlgorithm::EdDSA.name()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - - Ok(jwk) - } -} +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::*; +use crate::StrongholdStorage; #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] @@ -110,6 +40,13 @@ impl JwkStorage for StrongholdStorage { let keytype: ProceduresKeyType = match key_type { StrongholdKeyType::Ed25519 => ProceduresKeyType::Ed25519, + StrongholdKeyType::Bls12381G2 => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "`{key_type}` is supported but `JwkStorageBbsPlusExt::generate_bbs` should be called instead." + )), + ) + } }; let key_id: KeyId = random_key_id(); @@ -143,8 +80,8 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold public key procedure failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; let public_key: Vec = procedure_result.into(); + persist_changes(self.as_secret_manager(), stronghold).await?; let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); @@ -157,7 +94,7 @@ impl JwkStorage for StrongholdStorage { } async fn insert(&self, jwk: Jwk) -> KeyStorageResult { - let key_type: StrongholdKeyType = StrongholdKeyType::try_from(&jwk)?; + let key_type = StrongholdKeyType::try_from(&jwk)?; if !jwk.is_private() { return Err( KeyStorageError::new(KeyStorageErrorKind::Unspecified) @@ -195,7 +132,8 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold write secret failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; + + persist_changes(self.as_secret_manager(), stronghold).await?; Ok(key_id) } @@ -270,7 +208,8 @@ impl JwkStorage for StrongholdStorage { if !deleted { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); } - persist_changes(self, stronghold).await?; + + persist_changes(self.as_secret_manager(), stronghold).await?; Ok(()) } @@ -290,134 +229,3 @@ impl JwkStorage for StrongholdStorage { Ok(exists) } } - -/// Generate a random alphanumeric string of len 32. -fn random_key_id() -> KeyId { - KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) -} - -/// Check that the key type can be used with the algorithm. -fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { - match (key_type, alg) { - (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), - (key_type, alg) => Err( - KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), - ), - } -} - -fn get_client(stronghold: &Stronghold) -> KeyStorageResult { - let client = stronghold.get_client(IDENTITY_CLIENT_PATH); - match client { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { - match stronghold.load_client(IDENTITY_CLIENT_PATH) { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => stronghold - .create_client(IDENTITY_CLIENT_PATH) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -async fn persist_changes( - secret_manager: &StrongholdStorage, - stronghold: MutexGuard<'_, Stronghold>, -) -> KeyStorageResult<()> { - stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("stronghold write client error") - .with_source(err) - })?; - // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. - drop(stronghold); - - match secret_manager.as_secret_manager() { - iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { - stronghold_manager - .write_stronghold_snapshot(None) - .await - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("writing to stronghold snapshot failed") - .with_source(err) - })?; - } - _ => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("secret manager is not of type stronghold"), - ) - } - }; - Ok(()) -} - -/// Key Types supported by the stronghold storage implementation. -#[derive(Debug, Copy, Clone)] -enum StrongholdKeyType { - Ed25519, -} - -impl StrongholdKeyType { - /// String representation of the key type. - const fn name(&self) -> &'static str { - match self { - StrongholdKeyType::Ed25519 => "Ed25519", - } - } -} - -impl Display for StrongholdKeyType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.name()) - } -} - -impl TryFrom<&KeyType> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(value: &KeyType) -> Result { - match value.as_str() { - ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), - _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), - } - } -} - -impl TryFrom<&Jwk> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(jwk: &Jwk) -> Result { - match jwk.kty() { - JwkType::Okp => { - let okp_params = jwk.try_okp_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") - .with_source(err) - })?; - match okp_params.try_ed_curve().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("only Ed curves are supported for signing") - .with_source(err) - })? { - EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), - curve => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{curve} not supported")), - ), - } - } - other => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("Jwk `kty` {other} not supported")), - ), - } - } -} diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..10fbe7faa0 --- /dev/null +++ b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_storage::key_storage::bls::*; +use identity_storage::key_storage::JwkStorage; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_storage::KeyType; +use identity_storage::ProofUpdateCtx; +use identity_verification::jwk::Jwk; +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::Products; +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::str::FromStr; +use zeroize::Zeroizing; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::*; +use crate::utils::*; +use crate::StrongholdStorage; + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkStorageBbsPlusExt for StrongholdStorage { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type = StrongholdKeyType::try_from(&key_type)?; + + if !matches!(key_type, StrongholdKeyType::Bls12381G2) { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{key_type} is not supported")), + ); + } + + if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); + } + + // Get a key id that's not already used. + let mut kid = random_key_id(); + while self.exists(&kid).await? { + kid = random_key_id(); + } + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + let target_key_location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + kid.to_string().as_bytes().to_vec(), + ); + let jwk = client + .exec_proc([], &target_key_location, |_| { + let (sk, pk) = generate_bbs_keypair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let public_jwk = encode_bls_jwk(&sk, &pk, alg).1; + + Ok(Products { + output: public_jwk, + secret: Zeroizing::new(sk.to_bytes().to_vec()), + }) + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Failed to execute stronghold procedure") + .with_source(e) + })?; + + persist_changes(self.as_secret_manager(), stronghold).await?; + + Ok(JwkGenOutput::new(kid, jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let pk = expand_bls_jwk(public_key) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .1; + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + // Ensure `sk` and `pk` matches. + if sk.public_key() != pk { + return Err(FatalProcedureError::from( + "`public_key` is not the public key of key with id `key_id`".to_owned(), + )); + } + let signature_result = + sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())); + // clean up `sk` to avoid leaking. + drop(Zeroizing::new(sk.to_bytes())); + signature_result + }) + .map(|sig| sig.to_vec()) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature failed") + .with_source(e) + }) + } + + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let signature_update_result = + update_bbs_signature(alg, signature, &sk, &ctx).map_err(|e| FatalProcedureError::from(e.to_string())); + drop(Zeroizing::new(sk.to_bytes())); + signature_update_result + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + .with_source(e) + }) + } +} diff --git a/identity_stronghold/src/stronghold_key_id.rs b/identity_stronghold/src/storage/stronghold_key_id.rs similarity index 98% rename from identity_stronghold/src/stronghold_key_id.rs rename to identity_stronghold/src/storage/stronghold_key_id.rs index dcd3755cab..f7b7aa6436 100644 --- a/identity_stronghold/src/stronghold_key_id.rs +++ b/identity_stronghold/src/storage/stronghold_key_id.rs @@ -1,7 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::stronghold_jwk_storage::IDENTITY_CLIENT_PATH; +use crate::utils::IDENTITY_CLIENT_PATH; use crate::StrongholdStorage; use async_trait::async_trait; use identity_storage::key_id_storage::KeyIdStorage; diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs new file mode 100644 index 0000000000..c78deb4d3a --- /dev/null +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -0,0 +1,109 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyType; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkType; + +pub const ED25519_KEY_TYPE_STR: &str = "Ed25519"; +/// The Ed25519 key type. +pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(ED25519_KEY_TYPE_STR); +pub const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; +/// The BLS12381G2 key type +pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(BLS12381G2_KEY_TYPE_STR); + +/// Key Types supported by the stronghold storage implementation. +#[derive(Debug, Copy, Clone)] +pub enum StrongholdKeyType { + Ed25519, + Bls12381G2, +} + +impl StrongholdKeyType { + /// String representation of the key type. + const fn name(&self) -> &'static str { + match self { + StrongholdKeyType::Ed25519 => ED25519_KEY_TYPE_STR, + StrongholdKeyType::Bls12381G2 => BLS12381G2_KEY_TYPE_STR, + } + } +} + +impl Display for StrongholdKeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl TryFrom<&KeyType> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(value: &KeyType) -> Result { + match value.as_str() { + ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), + BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::Bls12381G2), + _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), + } + } +} + +impl From for KeyType { + fn from(key_type: StrongholdKeyType) -> KeyType { + KeyType::from_static_str(key_type.name()) + } +} + +impl TryFrom<&Jwk> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(jwk: &Jwk) -> Result { + match jwk.kty() { + JwkType::Okp => { + let okp_params = jwk.try_okp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") + .with_source(err) + })?; + match okp_params.try_ed_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected EC parameters for a JWK with `kty` Ec") + .with_source(err) + })?; + match ec_params.try_bls_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::Bls12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } + other => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("Jwk `kty` {other} not supported")), + ), + } + } +} diff --git a/identity_stronghold/src/tests/mod.rs b/identity_stronghold/src/tests/mod.rs index 96db03f0aa..54c5488c05 100644 --- a/identity_stronghold/src/tests/mod.rs +++ b/identity_stronghold/src/tests/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod test_bbs_ext; mod test_jwk_storage; mod test_key_id_storage; pub(crate) mod utils; diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs new file mode 100644 index 0000000000..efa71f3cc2 --- /dev/null +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_storage::key_storage::bls::expand_bls_jwk; +use identity_storage::key_storage::bls::sign_bbs; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorage; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyStorageErrorKind; +use iota_stronghold::procedures::Runner; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use rand::RngCore; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::tests::utils::create_stronghold_secret_manager; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; +use crate::StrongholdStorage; + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + + assert!(jwk.is_public()); + assert!(stronghold_storage.exists(&key_id).await?); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_key_type() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::Ed25519.into(), ProofAlgorithm::BLS12381_SHA256) + .await + .unwrap_err(); + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedKeyType)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_alg() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::MAC_H256) + .await + .unwrap_err(); + + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedProofAlgorithm)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_sign_bbs_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + let pk = expand_bls_jwk(&jwk)?.1; + let sk = { + let stronghold = stronghold_storage.get_stronghold().await; + let client = get_client(&stronghold)?; + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_owned(), + record_path: key_id.as_str().as_bytes().to_owned(), + }; + client + .get_guards([sk_location], |[sk]| Ok(BBSplusSecretKey::from_bytes(&sk.borrow()))) + .unwrap() + }?; + + let mut data = vec![0; 1024]; + rand::thread_rng().fill_bytes(&mut data); + let expected_signature = sign_bbs( + ProofAlgorithm::BLS12381_SHA256, + std::slice::from_ref(&data), + &sk, + &pk, + &[], + )?; + + let signature = stronghold_storage.sign_bbs(&key_id, &[data], &[], &jwk).await?; + assert_eq!(signature, expected_signature); + + Ok(()) +} diff --git a/identity_stronghold/src/tests/test_jwk_storage.rs b/identity_stronghold/src/tests/test_jwk_storage.rs index 6f3c9a7c5d..e7ccbb2a05 100644 --- a/identity_stronghold/src/tests/test_jwk_storage.rs +++ b/identity_stronghold/src/tests/test_jwk_storage.rs @@ -33,7 +33,10 @@ async fn retrieve() { .unwrap(); let key_id = &generate.key_id; - let pub_key: Jwk = stronghold_storage.get_public_key(key_id).await.unwrap(); + let pub_key: Jwk = stronghold_storage + .get_public_key_with_type(key_id, crate::stronghold_key_type::StrongholdKeyType::Ed25519) + .await + .unwrap(); assert_eq!(generate.jwk, pub_key); } diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs new file mode 100644 index 0000000000..3a9ae72842 --- /dev/null +++ b/identity_stronghold/src/utils.rs @@ -0,0 +1,87 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jws::JwsAlgorithm; +use iota_sdk::client::secret::SecretManager; +use iota_stronghold::Client; +use iota_stronghold::ClientError; +use iota_stronghold::Stronghold; +use rand::distributions::DistString as _; +use tokio::sync::MutexGuard; + +use crate::stronghold_key_type::StrongholdKeyType; + +pub static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; +pub static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; + +/// Generate a random alphanumeric string of len 32. +pub fn random_key_id() -> KeyId { + KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) +} + +/// Check that the key type can be used with the algorithm. +pub fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { + match (key_type, alg) { + (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), + (key_type, alg) => Err( + KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), + ), + } +} + +pub fn get_client(stronghold: &Stronghold) -> KeyStorageResult { + let client = stronghold.get_client(IDENTITY_CLIENT_PATH); + match client { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { + match stronghold.load_client(IDENTITY_CLIENT_PATH) { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => stronghold + .create_client(IDENTITY_CLIENT_PATH) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +pub async fn persist_changes( + secret_manager: &SecretManager, + stronghold: MutexGuard<'_, Stronghold>, +) -> KeyStorageResult<()> { + stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("stronghold write client error") + .with_source(err) + })?; + // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. + drop(stronghold); + + match secret_manager { + iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { + stronghold_manager + .write_stronghold_snapshot(None) + .await + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("writing to stronghold snapshot failed") + .with_source(err) + })?; + } + _ => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("secret manager is not of type stronghold"), + ) + } + }; + Ok(()) +}