diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 339ae53cbe..3b060b50c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -317,6 +317,22 @@ jobs: with: name: "docker-services-${{ github.job }}" + test-integration-aries-vcx-ledger: + needs: workflow-setup + if: ${{ needs.workflow-setup.outputs.SKIP_CI != 'true' }} + runs-on: ubuntu-20.04 + steps: + - name: "Git checkout" + uses: actions/checkout@v3 + - name: "Setup rust testing environment" + uses: ./.github/actions/setup-testing-rust + with: + rust-toolchain-version: ${{ env.RUST_TOOLCHAIN_VERSION }} + - name: "Install just" + run: sudo snap install --edge --classic just + - name: "Run aries-vcx-ledger integration tests" + run: just test-integration-aries-vcx-ledger + test-integration-did-crate: needs: workflow-setup if: ${{ needs.workflow-setup.outputs.SKIP_CI != 'true' }} diff --git a/Cargo.lock b/Cargo.lock index 33bfd98d4d..ed0a9ef616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -603,18 +603,25 @@ dependencies = [ "anoncreds_types", "aries_vcx_wallet", "async-trait", + "bitvec", + "chrono", + "did_cheqd", "did_parser_nom", + "did_resolver", "indy-ledger-response-parser", "indy-vdr", "indy-vdr-proxy-client", "log", "lru", + "mockall", "public_key", "serde", "serde_json", "thiserror", "time", "tokio", + "url", + "uuid", ] [[package]] @@ -1799,6 +1806,7 @@ dependencies = [ "env_logger 0.11.5", "log", "nom", + "percent-encoding", "serde", "serde_test", ] @@ -1841,6 +1849,7 @@ dependencies = [ "did_parser_nom", "serde", "serde_json", + "typed-builder", ] [[package]] @@ -3352,9 +3361,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" dependencies = [ "cfg-if", "downcast", @@ -3366,9 +3375,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" dependencies = [ "cfg-if", "proc-macro2", diff --git a/aries/aries_vcx/src/common/credentials/mod.rs b/aries/aries_vcx/src/common/credentials/mod.rs index f95cccb2eb..7b127273b6 100644 --- a/aries/aries_vcx/src/common/credentials/mod.rs +++ b/aries/aries_vcx/src/common/credentials/mod.rs @@ -25,6 +25,7 @@ pub async fn is_cred_revoked( rev_id: u32, ) -> VcxResult { let to = Some(OffsetDateTime::now_utc().unix_timestamp() as u64 + 100); + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 let (rev_reg_delta, _) = ledger .get_rev_reg_delta_json(&rev_reg_id.try_into()?, None, to) .await?; diff --git a/aries/aries_vcx/src/common/primitives/revocation_registry.rs b/aries/aries_vcx/src/common/primitives/revocation_registry.rs index 1b117e119c..46244915ae 100644 --- a/aries/aries_vcx/src/common/primitives/revocation_registry.rs +++ b/aries/aries_vcx/src/common/primitives/revocation_registry.rs @@ -234,6 +234,7 @@ impl RevocationRegistry { ledger: &impl AnoncredsLedgerRead, cred_rev_id: u32, ) -> VcxResult<()> { + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 let rev_reg_delta_json = ledger .get_rev_reg_delta_json(&self.rev_reg_id.to_string().try_into()?, None, None) .await? diff --git a/aries/aries_vcx/src/common/proofs/prover/prover_internal.rs b/aries/aries_vcx/src/common/proofs/prover/prover_internal.rs index 1f78080675..72947ffb37 100644 --- a/aries/aries_vcx/src/common/proofs/prover/prover_internal.rs +++ b/aries/aries_vcx/src/common/proofs/prover/prover_internal.rs @@ -12,6 +12,7 @@ use aries_vcx_anoncreds::anoncreds::base_anoncreds::{ BaseAnonCreds, CredentialDefinitionsMap, RevocationStatesMap, SchemasMap, }; use aries_vcx_ledger::ledger::base_ledger::AnoncredsLedgerRead; +use chrono::Utc; use crate::errors::error::prelude::*; @@ -160,26 +161,25 @@ pub async fn build_rev_states_json( if !rtn.contains_key(rev_reg_id) { // Does this make sense in case cred_info's for same rev_reg_ids have different // revocation intervals - let (from, to) = if let Some(ref interval) = cred_info.revocation_interval { + let (_from, to) = if let Some(ref interval) = cred_info.revocation_interval { (interval.from, interval.to) } else { (None, None) }; - let rev_reg_def_json = ledger_read - .get_rev_reg_def_json(&rev_reg_id.to_owned().try_into()?) - .await?; + let parsed_id = &rev_reg_id.to_owned().try_into()?; + let (rev_reg_def_json, meta) = ledger_read.get_rev_reg_def_json(parsed_id).await?; - let (rev_reg_delta_json, timestamp) = ledger_read - .get_rev_reg_delta_json(&rev_reg_id.to_owned().try_into()?, from, to) + let on_or_before = to.unwrap_or(Utc::now().timestamp() as u64); + let (rev_status_list, timestamp) = ledger_read + .get_rev_status_list(parsed_id, on_or_before, Some(&meta)) .await?; let rev_state_json = anoncreds .create_revocation_state( Path::new(tails_dir), rev_reg_def_json, - rev_reg_delta_json, - timestamp, + rev_status_list, *cred_rev_id, ) .await?; diff --git a/aries/aries_vcx/src/common/proofs/verifier/verifier_internal.rs b/aries/aries_vcx/src/common/proofs/verifier/verifier_internal.rs index 5a474f6b79..bcd7fd31b6 100644 --- a/aries/aries_vcx/src/common/proofs/verifier/verifier_internal.rs +++ b/aries/aries_vcx/src/common/proofs/verifier/verifier_internal.rs @@ -167,7 +167,7 @@ pub async fn build_rev_reg_defs_json( ))?; if rev_reg_defs_json.get(rev_reg_id).is_none() { - let json = ledger + let (json, _meta) = ledger .get_rev_reg_def_json(&rev_reg_id.to_string().try_into()?) .await?; let rev_reg_def_json = serde_json::to_value(&json).or(Err(AriesVcxError::from_msg( diff --git a/aries/aries_vcx/src/errors/mapping_ledger.rs b/aries/aries_vcx/src/errors/mapping_ledger.rs index deacc40b9a..3b540b3214 100644 --- a/aries/aries_vcx/src/errors/mapping_ledger.rs +++ b/aries/aries_vcx/src/errors/mapping_ledger.rs @@ -8,7 +8,7 @@ impl From for AriesVcxError { VcxLedgerError::LedgerItemNotFound => { Self::from_msg(AriesVcxErrorKind::LedgerItemNotFound, value) } - VcxLedgerError::InvalidLedgerResponse => { + VcxLedgerError::InvalidLedgerResponse(_) => { Self::from_msg(AriesVcxErrorKind::InvalidLedgerResponse, value) } VcxLedgerError::DuplicationSchema => { @@ -33,7 +33,9 @@ impl From for AriesVcxError { Self::from_msg(AriesVcxErrorKind::PoolLedgerConnect, value) } VcxLedgerError::IOError(_) => Self::from_msg(AriesVcxErrorKind::IOError, value), - VcxLedgerError::InvalidInput(_) | VcxLedgerError::IndyVdrValidation(_) => { + VcxLedgerError::InvalidInput(_) + | VcxLedgerError::IndyVdrValidation(_) + | VcxLedgerError::UnsupportedLedgerIdentifier(_) => { Self::from_msg(AriesVcxErrorKind::InvalidInput, value) } VcxLedgerError::UnknownError(_) => { diff --git a/aries/aries_vcx/src/handlers/issuance/issuer.rs b/aries/aries_vcx/src/handlers/issuance/issuer.rs index cec5e72733..df490f6847 100644 --- a/aries/aries_vcx/src/handlers/issuance/issuer.rs +++ b/aries/aries_vcx/src/handlers/issuance/issuer.rs @@ -241,6 +241,7 @@ impl Issuer { revocation_info.rev_reg_id, revocation_info.tails_file, ) { + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 let rev_reg_delta_json = ledger .get_rev_reg_delta_json(&rev_reg_id.to_owned().try_into()?, None, None) .await? diff --git a/aries/aries_vcx/src/protocols/issuance/holder/state_machine.rs b/aries/aries_vcx/src/protocols/issuance/holder/state_machine.rs index 7b534dd140..f44a236f1d 100644 --- a/aries/aries_vcx/src/protocols/issuance/holder/state_machine.rs +++ b/aries/aries_vcx/src/protocols/issuance/holder/state_machine.rs @@ -1,5 +1,8 @@ use std::fmt; +use anoncreds_types::data_types::{ + identifiers::schema_id::SchemaId, messages::cred_offer::CredentialOffer, +}; use aries_vcx_anoncreds::anoncreds::base_anoncreds::BaseAnonCreds; use aries_vcx_ledger::ledger::base_ledger::AnoncredsLedgerRead; use aries_vcx_wallet::wallet::base_wallet::BaseWallet; @@ -224,11 +227,12 @@ impl HolderSM { ) .await { - Ok((msg_credential_request, req_meta, cred_def_json)) => { + Ok((msg_credential_request, req_meta, cred_def_json, schema_id)) => { HolderFullState::RequestSet(RequestSetState { msg_credential_request, req_meta, cred_def_json, + schema_id, }) } Err(err) => { @@ -276,6 +280,8 @@ impl HolderSM { trace!("HolderSM::receive_credential >>"); let state = match self.state { HolderFullState::RequestSet(state_data) => { + let schema = ledger.get_schema(&state_data.schema_id, None).await?; + let schema_json = serde_json::to_string(&schema)?; match _store_credential( wallet, ledger, @@ -283,6 +289,7 @@ impl HolderSM { &credential, &state_data.req_meta, &state_data.cred_def_json, + &schema_json, ) .await { @@ -549,6 +556,7 @@ async fn _store_credential( credential: &IssueCredentialV1, req_meta: &str, cred_def_json: &str, + schema_json: &str, ) -> VcxResult<(String, Option)> { trace!( "Holder::_store_credential >>> credential: {:?}, req_meta: {}, cred_def_json: {}", @@ -561,7 +569,7 @@ async fn _store_credential( let rev_reg_id = _parse_rev_reg_id_from_credential(&credential_json)?; let rev_reg_def_json = if let Some(rev_reg_id) = rev_reg_id { - let json = ledger.get_rev_reg_def_json(&rev_reg_id.try_into()?).await?; + let (json, _meta) = ledger.get_rev_reg_def_json(&rev_reg_id.try_into()?).await?; Some(json) } else { None @@ -572,6 +580,7 @@ async fn _store_credential( wallet, serde_json::from_str(req_meta)?, serde_json::from_str(&credential_json)?, + serde_json::from_str(schema_json)?, serde_json::from_str(cred_def_json)?, rev_reg_def_json.clone(), ) @@ -585,24 +594,27 @@ async fn _store_credential( )) } +/// On success, returns: credential request, request metadata, cred_def_id, cred def, schema_id pub async fn create_anoncreds_credential_request( wallet: &impl BaseWallet, ledger: &impl AnoncredsLedgerRead, anoncreds: &impl BaseAnonCreds, - cred_def_id: &str, prover_did: &Did, cred_offer: &str, -) -> VcxResult<(String, String, String, String)> { - let cred_def_json = ledger - .get_cred_def(&cred_def_id.to_string().try_into()?, None) - .await?; +) -> VcxResult<(String, String, String, String, SchemaId)> { + let offer: CredentialOffer = serde_json::from_str(cred_offer)?; + + let schema_id = offer.schema_id.clone(); + let cred_def_id = offer.cred_def_id.clone(); + + let cred_def_json = ledger.get_cred_def(&cred_def_id, None).await?; let master_secret_id = settings::DEFAULT_LINK_SECRET_ALIAS; anoncreds .prover_create_credential_req( wallet, prover_did, - serde_json::from_str(cred_offer)?, + offer, cred_def_json.try_clone()?, &master_secret_id.to_string(), ) @@ -619,10 +631,13 @@ pub async fn create_anoncreds_credential_request( serde_json::to_string(&s2).unwrap(), cred_def_id.to_string(), serde_json::to_string(&cred_def_json).unwrap(), + schema_id, ) }) } +/// On success, returns: message with cred request, request metadata, cred def (for caching), +/// schema_id async fn build_credential_request_msg( wallet: &impl BaseWallet, ledger: &impl AnoncredsLedgerRead, @@ -630,7 +645,7 @@ async fn build_credential_request_msg( thread_id: String, my_pw_did: Did, offer: &OfferCredentialV1, -) -> VcxResult<(RequestCredentialV1, String, String)> { +) -> VcxResult<(RequestCredentialV1, String, String, SchemaId)> { trace!( "Holder::_make_credential_request >>> my_pw_did: {:?}, offer: {:?}", my_pw_did, @@ -640,17 +655,10 @@ async fn build_credential_request_msg( let cred_offer = get_attach_as_string!(&offer.content.offers_attach); trace!("Parsed cred offer attachment: {}", cred_offer); - let cred_def_id = parse_cred_def_id_from_cred_offer(&cred_offer)?; - let (req, req_meta, _cred_def_id, cred_def_json) = create_anoncreds_credential_request( - wallet, - ledger, - anoncreds, - &cred_def_id, - &my_pw_did, - &cred_offer, - ) - .await?; + let (req, req_meta, _cred_def_id, cred_def_json, schema_id) = + create_anoncreds_credential_request(wallet, ledger, anoncreds, &my_pw_did, &cred_offer) + .await?; trace!("Created cred def json: {}", cred_def_json); let credential_request_msg = _build_credential_request_msg(req, &thread_id); - Ok((credential_request_msg, req_meta, cred_def_json)) + Ok((credential_request_msg, req_meta, cred_def_json, schema_id)) } diff --git a/aries/aries_vcx/src/protocols/issuance/holder/states/request_set.rs b/aries/aries_vcx/src/protocols/issuance/holder/states/request_set.rs index e348378872..b92044d3f3 100644 --- a/aries/aries_vcx/src/protocols/issuance/holder/states/request_set.rs +++ b/aries/aries_vcx/src/protocols/issuance/holder/states/request_set.rs @@ -1,3 +1,4 @@ +use anoncreds_types::data_types::identifiers::schema_id::SchemaId; use messages::msg_fields::protocols::cred_issuance::v1::{ issue_credential::IssueCredentialV1, request_credential::RequestCredentialV1, }; @@ -11,6 +12,7 @@ use crate::{ pub struct RequestSetState { pub req_meta: String, pub cred_def_json: String, + pub schema_id: SchemaId, pub msg_credential_request: RequestCredentialV1, } diff --git a/aries/aries_vcx/tests/test_anoncreds.rs b/aries/aries_vcx/tests/test_anoncreds.rs index 4f742bd327..6155deb463 100644 --- a/aries/aries_vcx/tests/test_anoncreds.rs +++ b/aries/aries_vcx/tests/test_anoncreds.rs @@ -73,6 +73,7 @@ async fn test_pool_proof_req_attribute_names() -> Result<(), Box> { Ok(()) } +#[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 #[tokio::test] #[ignore] async fn test_pool_revoke_credential() -> Result<(), Box> { @@ -109,6 +110,7 @@ async fn test_pool_revoke_credential() -> Result<(), Box> { &setup.anoncreds, &setup.anoncreds, &setup.institution_did, + &schema, &cred_def, Some(&rev_reg), ) diff --git a/aries/aries_vcx/tests/test_credential_retrieval.rs b/aries/aries_vcx/tests/test_credential_retrieval.rs index e43af06c3c..12cfbe5d15 100644 --- a/aries/aries_vcx/tests/test_credential_retrieval.rs +++ b/aries/aries_vcx/tests/test_credential_retrieval.rs @@ -147,6 +147,7 @@ async fn test_agency_pool_case_for_proof_req_doesnt_matter_for_retrieve_creds( &setup.anoncreds, &setup.anoncreds, &setup.institution_did, + &schema, &cred_def, None, ) diff --git a/aries/aries_vcx/tests/test_credentials.rs b/aries/aries_vcx/tests/test_credentials.rs index c0191e74eb..cd5ef078b9 100644 --- a/aries/aries_vcx/tests/test_credentials.rs +++ b/aries/aries_vcx/tests/test_credentials.rs @@ -48,6 +48,7 @@ async fn test_pool_prover_get_credential() -> Result<(), Box> { &setup.anoncreds, &setup.anoncreds, &setup.institution_did, + &schema, &cred_def, Some(&rev_reg), ) @@ -102,6 +103,7 @@ async fn test_pool_is_cred_revoked() -> Result<(), Box> { &setup.anoncreds, &setup.anoncreds, &setup.institution_did, + &schema, &cred_def, Some(&rev_reg), ) @@ -110,6 +112,7 @@ async fn test_pool_is_cred_revoked() -> Result<(), Box> { assert!(!is_cred_revoked(&setup.ledger_read, &rev_reg.rev_reg_id, cred_rev_id).await?); + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 let rev_reg_delta_json = setup .ledger_read .get_rev_reg_delta_json(&rev_reg.rev_reg_id.to_owned().try_into()?, None, None) diff --git a/aries/aries_vcx/tests/test_pool.rs b/aries/aries_vcx/tests/test_pool.rs index 87a44db890..ac2cfe5b1d 100644 --- a/aries/aries_vcx/tests/test_pool.rs +++ b/aries/aries_vcx/tests/test_pool.rs @@ -475,6 +475,7 @@ async fn test_pool_get_rev_reg_delta_json() -> Result<(), Box> { .await?; let ledger = &setup.ledger_read; + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 let (_delta, _timestamp) = ledger .get_rev_reg_delta_json(&rev_reg.rev_reg_id.to_owned().try_into()?, None, None) .await?; diff --git a/aries/aries_vcx/tests/test_proof_presentation.rs b/aries/aries_vcx/tests/test_proof_presentation.rs index 3053450abd..2609da0175 100644 --- a/aries/aries_vcx/tests/test_proof_presentation.rs +++ b/aries/aries_vcx/tests/test_proof_presentation.rs @@ -71,6 +71,7 @@ async fn test_agency_pool_generate_proof_with_predicates() -> Result<(), Box, ) -> String { @@ -159,6 +161,7 @@ pub async fn create_and_write_credential( wallet_holder, req_meta, cred, + schema.schema_json.clone(), cred_def.get_cred_def_json().try_clone().unwrap(), rev_reg_def_json .as_deref() diff --git a/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/mod.rs b/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/mod.rs index d32a9114cd..5b83239d47 100644 --- a/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/mod.rs +++ b/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/mod.rs @@ -33,39 +33,41 @@ use anoncreds::{ RevocationRegistryDefinition as AnoncredsRevocationRegistryDefinition, }, }; -use anoncreds_types::data_types::{ - identifiers::{ - cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, - schema_id::SchemaId, - }, - ledger::{ - cred_def::{CredentialDefinition, SignatureType}, - rev_reg::RevocationRegistry, - rev_reg_def::RevocationRegistryDefinition, - rev_reg_delta::{RevocationRegistryDelta, RevocationRegistryDeltaValue}, - rev_status_list::RevocationStatusList, - schema::{AttributeNames, Schema}, - }, - messages::{ - cred_definition_config::CredentialDefinitionConfig, - cred_offer::CredentialOffer, - cred_request::{CredentialRequest, CredentialRequestMetadata}, - cred_selection::{ - RetrievedCredentialForReferent, RetrievedCredentialInfo, RetrievedCredentials, +use anoncreds_types::{ + data_types::{ + identifiers::{ + cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, + schema_id::SchemaId, + }, + ledger::{ + cred_def::{CredentialDefinition, SignatureType}, + rev_reg::RevocationRegistry, + rev_reg_def::RevocationRegistryDefinition, + rev_reg_delta::{RevocationRegistryDelta, RevocationRegistryDeltaValue}, + rev_status_list::RevocationStatusList, + schema::{AttributeNames, Schema}, + }, + messages::{ + cred_definition_config::CredentialDefinitionConfig, + cred_offer::CredentialOffer, + cred_request::{CredentialRequest, CredentialRequestMetadata}, + cred_selection::{ + RetrievedCredentialForReferent, RetrievedCredentialInfo, RetrievedCredentials, + }, + credential::{Credential, CredentialValues}, + nonce::Nonce, + pres_request::PresentationRequest, + presentation::{Presentation, RequestedCredentials}, + revocation_state::CredentialRevocationState, }, - credential::{Credential, CredentialValues}, - nonce::Nonce, - pres_request::PresentationRequest, - presentation::{Presentation, RequestedCredentials}, - revocation_state::CredentialRevocationState, }, + utils::conversions::from_revocation_registry_delta_to_revocation_status_list, }; use aries_vcx_wallet::wallet::{ base_wallet::{record::Record, record_category::RecordCategory, BaseWallet}, record_tags::{RecordTag, RecordTags}, }; use async_trait::async_trait; -use bitvec::bitvec; use did_parser_nom::Did; use log::warn; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -82,36 +84,6 @@ use crate::{ errors::error::{VcxAnoncredsError, VcxAnoncredsResult}, }; -fn from_revocation_registry_delta_to_revocation_status_list( - delta: &RevocationRegistryDeltaValue, - rev_reg_def: &AnoncredsRevocationRegistryDefinition, - rev_reg_def_id: &RevocationRegistryDefinitionId, - timestamp: Option, - issuance_by_default: bool, -) -> VcxAnoncredsResult { - let default_state = if issuance_by_default { 0 } else { 1 }; - let mut revocation_list = bitvec![default_state; rev_reg_def.value.max_cred_num as usize]; - - for issued in &delta.issued { - revocation_list.insert(*issued as usize, false); - } - - for revoked in &delta.revoked { - revocation_list.insert(*revoked as usize, true); - } - - let accum = delta.accum.into(); - - RevocationStatusList::new( - Some(&rev_reg_def_id.to_string()), - rev_reg_def.issuer_id.clone().convert(())?, - revocation_list, - Some(accum), - timestamp, - ) - .map_err(Into::into) -} - fn from_revocation_status_list_to_revocation_registry_delta( rev_status_list: &RevocationStatusList, prev_accum: Option, @@ -288,17 +260,35 @@ impl BaseAnonCreds for Anoncreds { let cred_defs: HashMap = credential_defs_json.convert(())?; + // tack on issuerId for ease of processing status lists + let rev_regs_map_with_issuer_ids: Option> = + match (rev_regs_json, &rev_reg_defs_json) { + (Some(regs), Some(defs)) => Some( + regs.into_iter() + .filter_map(|(k, v)| { + let def = defs.get(&k)?; + Some((k, (v, def.issuer_id.clone()))) + }) + .collect(), + ), + _ => None, + }; + let rev_reg_defs: Option< HashMap, > = rev_reg_defs_json.map(|v| v.convert(())).transpose()?; + let rev_status_lists = rev_regs_map_with_issuer_ids + .map(|r| r.convert(())) + .transpose()?; + Ok(anoncreds::verifier::verify_presentation( &presentation, &pres_req, &schemas, &cred_defs, rev_reg_defs.as_ref(), - rev_regs_json.map(|r| r.convert(())).transpose()?, + rev_status_lists, None, // no idea what this is )?) } @@ -947,18 +937,9 @@ impl BaseAnonCreds for Anoncreds { &self, tails_dir: &Path, rev_reg_def_json: RevocationRegistryDefinition, - rev_reg_delta_json: RevocationRegistryDelta, - timestamp: u64, + rev_status_list: RevocationStatusList, cred_rev_id: u32, ) -> VcxAnoncredsResult { - let cred_def_id = rev_reg_def_json.cred_def_id.to_string(); - let max_cred_num = rev_reg_def_json.value.max_cred_num; - let rev_reg_def_id = rev_reg_def_json.id.to_string(); - let (_cred_def_method, issuer_did, _signature_type, _schema_num, _tag) = - cred_def_parts(&cred_def_id).ok_or(VcxAnoncredsError::InvalidSchema(format!( - "Could not process cred_def_id {cred_def_id} as parts." - )))?; - let revoc_reg_def: AnoncredsRevocationRegistryDefinition = rev_reg_def_json.convert(())?; let tails_file_hash = revoc_reg_def.value.tails_hash.as_str(); @@ -970,25 +951,6 @@ impl BaseAnonCreds for Anoncreds { VcxAnoncredsError::InvalidOption("tails file is not an unicode string".into()) })?; - let RevocationRegistryDeltaValue { accum, revoked, .. } = rev_reg_delta_json.value; - - let issuer_id = IssuerId::new(issuer_did.did()).unwrap(); - let mut revocation_list = bitvec!(0; max_cred_num as usize); - revoked.into_iter().for_each(|id| { - revocation_list - .get_mut(id as usize) - .map(|mut b| *b = true) - .unwrap_or_default() - }); - let registry = CryptoRevocationRegistry { accum }; - - let rev_status_list = RevocationStatusList::new( - Some(&rev_reg_def_id), - issuer_id.convert(())?, - revocation_list, - Some(registry), - Some(timestamp), - )?; let rev_state = anoncreds::prover::create_or_update_revocation_state( tails_path, &revoc_reg_def, @@ -1004,26 +966,21 @@ impl BaseAnonCreds for Anoncreds { async fn prover_store_credential( &self, wallet: &impl BaseWallet, - cred_req_metadata_json: CredentialRequestMetadata, - cred_json: Credential, - cred_def_json: CredentialDefinition, - rev_reg_def_json: Option, + cred_req_metadata: CredentialRequestMetadata, + unprocessed_cred: Credential, + schema: Schema, + cred_def: CredentialDefinition, + rev_reg_def: Option, ) -> VcxAnoncredsResult { - let mut credential: AnoncredsCredential = cred_json.convert(())?; - - let cred_def_id = credential.cred_def_id.to_string(); - let (_cred_def_method, issuer_did, _signature_type, _schema_num, _tag) = - cred_def_parts(&cred_def_id).ok_or(VcxAnoncredsError::InvalidSchema( - "Could not process credential.cred_def_id as parts.".into(), - ))?; + let mut credential: AnoncredsCredential = unprocessed_cred.convert(())?; let cred_request_metadata: AnoncredsCredentialRequestMetadata = - cred_req_metadata_json.convert(())?; + cred_req_metadata.convert(())?; let link_secret_id = &cred_request_metadata.link_secret_name; let link_secret = self.get_link_secret(wallet, link_secret_id).await?; - let cred_def: AnoncredsCredentialDefinition = cred_def_json.convert(())?; + let cred_def: AnoncredsCredentialDefinition = cred_def.convert(())?; let rev_reg_def: Option = - if let Some(rev_reg_def_json) = rev_reg_def_json { + if let Some(rev_reg_def_json) = rev_reg_def { Some(rev_reg_def_json.convert(())?) } else { None @@ -1038,19 +995,20 @@ impl BaseAnonCreds for Anoncreds { )?; let schema_id = &credential.schema_id; + let cred_def_id = &credential.cred_def_id; + let issuer_did = &cred_def.issuer_id; - let (_schema_method, schema_issuer_did, schema_name, schema_version) = - schema_parts(schema_id.0.as_str()).ok_or(VcxAnoncredsError::InvalidSchema(format!( - "Could not process credential.schema_id {schema_id} as parts." - )))?; + let schema_issuer_did = schema.issuer_id; + let schema_name = schema.name; + let schema_version = schema.version; let mut tags = RecordTags::new(vec![ RecordTag::new("schema_id", &schema_id.0), - RecordTag::new("schema_issuer_did", schema_issuer_did.did()), + RecordTag::new("schema_issuer_did", &schema_issuer_did.0), RecordTag::new("schema_name", &schema_name), RecordTag::new("schema_version", &schema_version), - RecordTag::new("issuer_did", issuer_did.did()), - RecordTag::new("cred_def_id", &cred_def_id), + RecordTag::new("issuer_did", &issuer_did.0), + RecordTag::new("cred_def_id", &cred_def_id.0), ]); if let Some(rev_reg_id) = &credential.rev_reg_id { @@ -1164,10 +1122,10 @@ impl BaseAnonCreds for Anoncreds { let current_time = OffsetDateTime::now_utc().unix_timestamp() as u64; let rev_status_list = from_revocation_registry_delta_to_revocation_status_list( &last_rev_reg_delta.value, - &rev_reg_def.clone().convert(())?, - rev_reg_id, - Some(current_time), - true, + current_time, + &rev_reg_def.id, + rev_reg_def.value.max_cred_num as usize, + rev_reg_def.issuer_id.clone(), )?; let cred_def = self @@ -1441,84 +1399,3 @@ pub fn schema_parts(id: &str) -> Option<(Option<&str>, Did, String, String)> { None } - -pub fn cred_def_parts(id: &str) -> Option<(Option<&str>, Did, String, SchemaId, String)> { - let parts = id.split_terminator(':').collect::>(); - - if parts.len() == 4 { - // Th7MpTaRZVRYnPiabds81Y:3:CL:1 - let did = parts[0].to_string(); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[2].to_string(); - let schema_id = parts[3].to_string(); - let tag = String::new(); - return Some((None, did, signature_type, SchemaId(schema_id), tag)); - } - - if parts.len() == 5 { - // Th7MpTaRZVRYnPiabds81Y:3:CL:1:tag - let did = parts[0].to_string(); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[2].to_string(); - let schema_id = parts[3].to_string(); - let tag = parts[4].to_string(); - return Some((None, did, signature_type, SchemaId(schema_id), tag)); - } - - if parts.len() == 7 { - // NcYxiDXkpYi6ov5FcYDi1e:3:CL:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0 - let did = parts[0].to_string(); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[2].to_string(); - let schema_id = parts[3..7].join(":"); - let tag = String::new(); - return Some((None, did, signature_type, SchemaId(schema_id), tag)); - } - - if parts.len() == 8 { - // NcYxiDXkpYi6ov5FcYDi1e:3:CL:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag - let did = parts[0].to_string(); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[2].to_string(); - let schema_id = parts[3..7].join(":"); - let tag = parts[7].to_string(); - return Some((None, did, signature_type, SchemaId(schema_id), tag)); - } - - if parts.len() == 9 { - // creddef:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:3:tag - let method = parts[1]; - let did = parts[2..5].join(":"); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[6].to_string(); - let schema_id = parts[7].to_string(); - let tag = parts[8].to_string(); - return Some((Some(method), did, signature_type, SchemaId(schema_id), tag)); - } - - if parts.len() == 16 { - // creddef:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:schema:sov:did:sov: - // NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag - let method = parts[1]; - let did = parts[2..5].join(":"); - let Ok(did) = Did::parse(did) else { - return None; - }; - let signature_type = parts[6].to_string(); - let schema_id = parts[7..15].join(":"); - let tag = parts[15].to_string(); - return Some((Some(method), did, signature_type, SchemaId(schema_id), tag)); - } - - None -} diff --git a/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/type_conversion.rs b/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/type_conversion.rs index 35c0762685..ef0958b3fc 100644 --- a/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/type_conversion.rs +++ b/aries/aries_vcx_anoncreds/src/anoncreds/anoncreds/type_conversion.rs @@ -264,9 +264,8 @@ impl Convert for OurRevocationRegistryDefinition { type Error = Box; fn convert(self, (): Self::Args) -> Result { - let issuer_id = self.id.to_string().split(':').next().unwrap().to_string(); Ok(AnoncredsRevocationRegistryDefinition { - issuer_id: AnoncredsIssuerId::new(issuer_id)?, + issuer_id: self.issuer_id.convert(())?, revoc_def_type: self.revoc_def_type.convert(())?, tag: self.tag, cred_def_id: AnoncredsCredentialDefinitionId::new(self.cred_def_id.to_string())?, @@ -288,6 +287,7 @@ impl Convert for AnoncredsRevocationRegistryDefinition { fn convert(self, (rev_reg_def_id,): Self::Args) -> Result { Ok(OurRevocationRegistryDefinition { id: OurRevocationRegistryDefinitionId::new(rev_reg_def_id)?, + issuer_id: self.issuer_id.convert(())?, revoc_def_type: self.revoc_def_type.convert(())?, tag: self.tag, cred_def_id: OurCredentialDefinitionId::new(self.cred_def_id.to_string())?, @@ -483,30 +483,26 @@ impl Convert for HashMap> { +impl Convert + for HashMap< + OurRevocationRegistryDefinitionId, + (HashMap, OurIssuerId), + > +{ type Args = (); type Target = Vec; type Error = Box; fn convert(self, _args: Self::Args) -> Result { let mut lists = Vec::new(); - for (rev_reg_def_id, timestamp_map) in self.into_iter() { + for (rev_reg_def_id, (timestamp_map, issuer_id)) in self.into_iter() { for (timestamp, entry) in timestamp_map { - let issuer_id = AnoncredsIssuerId::new( - rev_reg_def_id - .to_string() - .split(':') - .next() - .unwrap() - .to_string(), - ) - .unwrap(); let OurRevocationRegistry { value } = entry; let registry = CryptoRevocationRegistry { accum: value.accum }; let rev_status_list = OurRevocationStatusList::new( Some(&rev_reg_def_id.to_string()), - issuer_id.convert(())?, + issuer_id.clone(), Default::default(), Some(registry), Some(timestamp), diff --git a/aries/aries_vcx_anoncreds/src/anoncreds/base_anoncreds.rs b/aries/aries_vcx_anoncreds/src/anoncreds/base_anoncreds.rs index 28cbb0a221..0dac88a6ef 100644 --- a/aries/aries_vcx_anoncreds/src/anoncreds/base_anoncreds.rs +++ b/aries/aries_vcx_anoncreds/src/anoncreds/base_anoncreds.rs @@ -10,6 +10,7 @@ use anoncreds_types::data_types::{ rev_reg::RevocationRegistry, rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, schema::{AttributeNames, Schema}, }, messages::{ @@ -138,18 +139,18 @@ pub trait BaseAnonCreds: std::fmt::Debug + Send + Sync { &self, tails_dir: &Path, rev_reg_def_json: RevocationRegistryDefinition, - rev_reg_delta_json: RevocationRegistryDelta, - timestamp: u64, + rev_status_list: RevocationStatusList, cred_rev_id: u32, ) -> VcxAnoncredsResult; async fn prover_store_credential( &self, wallet: &impl BaseWallet, - cred_req_metadata_json: CredentialRequestMetadata, - cred_json: Credential, - cred_def_json: CredentialDefinition, - rev_reg_def_json: Option, + cred_req_metadata: CredentialRequestMetadata, + unprocessed_cred: Credential, + schema: Schema, + cred_def: CredentialDefinition, + rev_reg_def: Option, ) -> VcxAnoncredsResult; async fn prover_delete_credential( @@ -175,6 +176,7 @@ pub trait BaseAnonCreds: std::fmt::Debug + Send + Sync { // TODO - FUTURE - think about moving this to somewhere else, as it aggregates other calls (not // PURE Anoncreds) // ^ YES + // TODO - review functionality below and convert to using statuslists (https://github.com/hyperledger/aries-vcx/issues/1309) async fn revoke_credential_local( &self, wallet: &impl BaseWallet, diff --git a/aries/aries_vcx_ledger/Cargo.toml b/aries/aries_vcx_ledger/Cargo.toml index d5a726ee86..8ddca850b0 100644 --- a/aries/aries_vcx_ledger/Cargo.toml +++ b/aries/aries_vcx_ledger/Cargo.toml @@ -9,6 +9,7 @@ edition.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] vdr_proxy_ledger = ["dep:indy-vdr-proxy-client"] +cheqd = ["dep:did_cheqd", "dep:did_resolver", "dep:url"] [dependencies] aries_vcx_wallet = { path = "../aries_vcx_wallet" } @@ -17,12 +18,26 @@ did_parser_nom = { path = "../../did_core/did_parser_nom" } thiserror = "1.0.40" indy-vdr.workspace = true indy-vdr-proxy-client = { workspace = true, optional = true } +did_cheqd = { path = "../../did_core/did_methods/did_cheqd", optional = true } +did_resolver = { path = "../../did_core/did_resolver", optional = true } +url = { version = "2.4.1", optional = true } serde_json = "1.0.95" -public_key = { path = "../../did_core/public_key"} +public_key = { path = "../../did_core/public_key" } async-trait = "0.1.68" time = "0.3.20" indy-ledger-response-parser = { path = "../misc/indy_ledger_response_parser" } log = "0.4.17" serde = { version = "1.0.159", features = ["derive"] } -lru = { version = "0.12.0" } +lru = { version = "0.12.0" } tokio = { version = "1.38" } +chrono = { version = "0.4", default-features = false, features = ["alloc"] } +bitvec = "1.0.1" + +[dev-dependencies] +tokio = { version = "1.38.0", default-features = false, features = [ + "macros", + "rt", +] } +chrono = { version = "0.4", default-features = true } +mockall = "0.13.1" +uuid = { version = "1.4.1", default-features = false, features = ["v4"] } diff --git a/aries/aries_vcx_ledger/src/errors/error.rs b/aries/aries_vcx_ledger/src/errors/error.rs index 9031cfdda4..d4281e542c 100644 --- a/aries/aries_vcx_ledger/src/errors/error.rs +++ b/aries/aries_vcx_ledger/src/errors/error.rs @@ -15,8 +15,8 @@ pub enum VcxLedgerError { IOError(#[source] VdrError), #[error("Ledger item not found")] LedgerItemNotFound, - #[error("Invalid ledger response")] - InvalidLedgerResponse, + #[error("Invalid ledger response {0}")] + InvalidLedgerResponse(String), #[error("Duplicated schema")] DuplicationSchema, #[error("Invalid JSON: {0}")] @@ -29,6 +29,8 @@ pub enum VcxLedgerError { InvalidOption(String), #[error("Invalid input: {0}")] InvalidInput(String), + #[error("Unsupported ledger identifier: {0}")] + UnsupportedLedgerIdentifier(String), #[error("Unknown error: {0}")] UnknownError(String), #[error("Indy Vdr Validation error: {0}")] diff --git a/aries/aries_vcx_ledger/src/errors/mapping_cheqd.rs b/aries/aries_vcx_ledger/src/errors/mapping_cheqd.rs new file mode 100644 index 0000000000..ded7c36f89 --- /dev/null +++ b/aries/aries_vcx_ledger/src/errors/mapping_cheqd.rs @@ -0,0 +1,31 @@ +use did_cheqd::error::{parsing::ParsingErrorSource, DidCheqdError}; + +use super::error::VcxLedgerError; + +impl From for VcxLedgerError { + fn from(value: DidCheqdError) -> Self { + match value { + DidCheqdError::MethodNotSupported(_) => VcxLedgerError::InvalidInput(value.to_string()), + DidCheqdError::NetworkNotSupported(_) => { + VcxLedgerError::InvalidInput(value.to_string()) + } + DidCheqdError::BadConfiguration(_) => VcxLedgerError::InvalidInput(value.to_string()), + DidCheqdError::TransportError(_) => { + VcxLedgerError::InvalidLedgerResponse(value.to_string()) + } + DidCheqdError::NonSuccessResponse(_) => { + VcxLedgerError::InvalidLedgerResponse(value.to_string()) + } + DidCheqdError::InvalidResponse(_) => { + VcxLedgerError::InvalidLedgerResponse(value.to_string()) + } + DidCheqdError::InvalidDidDocument(_) => VcxLedgerError::InvalidInput(value.to_string()), + DidCheqdError::InvalidDidUrl(_) => VcxLedgerError::InvalidInput(value.to_string()), + DidCheqdError::ParsingError(ParsingErrorSource::DidDocumentParsingError(e)) => { + VcxLedgerError::ParseError(e) + } + DidCheqdError::Other(_) => VcxLedgerError::UnknownError(value.to_string()), + _ => VcxLedgerError::UnknownError(value.to_string()), + } + } +} diff --git a/aries/aries_vcx_ledger/src/errors/mapping_indyvdr.rs b/aries/aries_vcx_ledger/src/errors/mapping_indyvdr.rs index fd28bf51e5..9c4cbf0957 100644 --- a/aries/aries_vcx_ledger/src/errors/mapping_indyvdr.rs +++ b/aries/aries_vcx_ledger/src/errors/mapping_indyvdr.rs @@ -19,7 +19,7 @@ impl From for VcxLedgerError { | VdrErrorKind::PoolNoConsensus | VdrErrorKind::Resolver | VdrErrorKind::PoolTimeout => Self::UnknownError(err.to_string()), - VdrErrorKind::PoolRequestFailed(_) => Self::InvalidLedgerResponse, + VdrErrorKind::PoolRequestFailed(_) => Self::InvalidLedgerResponse(err.to_string()), } } } diff --git a/aries/aries_vcx_ledger/src/errors/mapping_indyvdr_proxy.rs b/aries/aries_vcx_ledger/src/errors/mapping_indyvdr_proxy.rs index a1527997a4..afabe18fc0 100644 --- a/aries/aries_vcx_ledger/src/errors/mapping_indyvdr_proxy.rs +++ b/aries/aries_vcx_ledger/src/errors/mapping_indyvdr_proxy.rs @@ -3,7 +3,7 @@ use indy_vdr_proxy_client::error::VdrProxyClientError; use super::error::VcxLedgerError; impl From for VcxLedgerError { - fn from(_err: VdrProxyClientError) -> Self { - Self::InvalidLedgerResponse + fn from(err: VdrProxyClientError) -> Self { + Self::InvalidLedgerResponse(err.to_string()) } } diff --git a/aries/aries_vcx_ledger/src/errors/mapping_ledger_response_parser.rs b/aries/aries_vcx_ledger/src/errors/mapping_ledger_response_parser.rs index c895ec383f..d1831e7b6d 100644 --- a/aries/aries_vcx_ledger/src/errors/mapping_ledger_response_parser.rs +++ b/aries/aries_vcx_ledger/src/errors/mapping_ledger_response_parser.rs @@ -8,7 +8,7 @@ impl From for VcxLedgerError { LedgerResponseParserError::JsonError(err) => VcxLedgerError::InvalidJson(err), LedgerResponseParserError::LedgerItemNotFound(_) => VcxLedgerError::LedgerItemNotFound, LedgerResponseParserError::InvalidTransaction(_) => { - VcxLedgerError::InvalidLedgerResponse + VcxLedgerError::InvalidLedgerResponse(err.to_string()) } } } diff --git a/aries/aries_vcx_ledger/src/errors/mod.rs b/aries/aries_vcx_ledger/src/errors/mod.rs index 22f23fcf66..ba15f6fe37 100644 --- a/aries/aries_vcx_ledger/src/errors/mod.rs +++ b/aries/aries_vcx_ledger/src/errors/mod.rs @@ -1,4 +1,6 @@ pub mod error; +#[cfg(feature = "cheqd")] +mod mapping_cheqd; mod mapping_indyvdr; #[cfg(feature = "vdr_proxy_ledger")] mod mapping_indyvdr_proxy; diff --git a/aries/aries_vcx_ledger/src/ledger/arc.rs b/aries/aries_vcx_ledger/src/ledger/arc.rs new file mode 100644 index 0000000000..9ca3568dca --- /dev/null +++ b/aries/aries_vcx_ledger/src/ledger/arc.rs @@ -0,0 +1,120 @@ +//! Contains convenience wrappers for the aries_vcx_ledger traits when working with [Arc]s. +use std::sync::Arc; + +use anoncreds_types::data_types::{ + identifiers::{ + cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, + schema_id::SchemaId, + }, + ledger::{ + cred_def::CredentialDefinition, rev_reg::RevocationRegistry, + rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, schema::Schema, + }, +}; +use async_trait::async_trait; +use did_parser_nom::Did; + +use super::base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerSupport}; +use crate::errors::error::VcxLedgerResult; + +/// Trait designed to convert [Arc] into [ArcLedgerTraitWrapper], such that +/// [Arc] can inherit any trait implementation of [ArcLedgerTraitWrapper]. (e.g. +/// [AnoncredsLedgerRead], [AnoncredsLedgerSupport]). +pub trait IntoArcLedgerTrait +where + Self: Sized, +{ + fn into_impl(self) -> ArcLedgerTraitWrapper; +} + +impl IntoArcLedgerTrait for Arc { + fn into_impl(self) -> ArcLedgerTraitWrapper { + ArcLedgerTraitWrapper(self) + } +} + +/// Thin wrapper over some [Arc]. Designed to implement relevant aries_vcx_ledger +/// traits on behalf of [Arc], if [T] implements those traits. Necessary since [Arc] +/// would not inherit those implementations automatically. +#[derive(Debug)] +pub struct ArcLedgerTraitWrapper(Arc); + +#[async_trait] +impl AnoncredsLedgerRead for ArcLedgerTraitWrapper +where + T: AnoncredsLedgerRead, +{ + type RevocationRegistryDefinitionAdditionalMetadata = + T::RevocationRegistryDefinitionAdditionalMetadata; + + async fn get_schema( + &self, + schema_id: &SchemaId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + self.0.get_schema(schema_id, submitter_did).await + } + + async fn get_cred_def( + &self, + cred_def_id: &CredentialDefinitionId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + self.0.get_cred_def(cred_def_id, submitter_did).await + } + async fn get_rev_reg_def_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + Self::RevocationRegistryDefinitionAdditionalMetadata, + )> { + self.0.get_rev_reg_def_json(rev_reg_id).await + } + + async fn get_rev_reg_delta_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + from: Option, + to: Option, + ) -> VcxLedgerResult<(RevocationRegistryDelta, u64)> { + #[allow(deprecated)] + self.0.get_rev_reg_delta_json(rev_reg_id, from, to).await + } + + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&Self::RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + self.0 + .get_rev_status_list(rev_reg_id, timestamp, rev_reg_def_meta) + .await + } + async fn get_rev_reg( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + ) -> VcxLedgerResult<(RevocationRegistry, u64)> { + self.0.get_rev_reg(rev_reg_id, timestamp).await + } +} + +impl AnoncredsLedgerSupport for ArcLedgerTraitWrapper +where + T: AnoncredsLedgerSupport, +{ + fn supports_schema(&self, id: &SchemaId) -> bool { + self.0.supports_schema(id) + } + + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool { + self.0.supports_credential_definition(id) + } + + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool { + self.0.supports_revocation_registry(id) + } +} diff --git a/aries/aries_vcx_ledger/src/ledger/base_ledger.rs b/aries/aries_vcx_ledger/src/ledger/base_ledger.rs index 3c34983cd5..d068b62b11 100644 --- a/aries/aries_vcx_ledger/src/ledger/base_ledger.rs +++ b/aries/aries_vcx_ledger/src/ledger/base_ledger.rs @@ -8,7 +8,7 @@ use anoncreds_types::data_types::{ ledger::{ cred_def::CredentialDefinition, rev_reg::RevocationRegistry, rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, - schema::Schema, + rev_status_list::RevocationStatusList, schema::Schema, }, }; use aries_vcx_wallet::wallet::base_wallet::BaseWallet; @@ -73,8 +73,16 @@ pub trait IndyLedgerWrite: Debug + Send + Sync { ) -> VcxLedgerResult; } +/// Trait for reading anoncreds-related objects from some ledger/s. #[async_trait] pub trait AnoncredsLedgerRead: Debug + Send + Sync { + /// Opaque additional metadata associated with retrieving a revocation registry definition. + /// Returned as part of `get_rev_reg_def_json`, and optionally passed into + /// `get_rev_status_list`. Depending on the ledger anoncreds-method, this metadata may be + /// used in the subsequent revocation status list fetch as an optimization (e.g. to save an + /// additional ledger call). + type RevocationRegistryDefinitionAdditionalMetadata: Send + Sync; + async fn get_schema( &self, schema_id: &SchemaId, @@ -85,16 +93,37 @@ pub trait AnoncredsLedgerRead: Debug + Send + Sync { cred_def_id: &CredentialDefinitionId, submitter_did: Option<&Did>, ) -> VcxLedgerResult; + + /// Get the anoncreds [RevocationRegistryDefinition] associated with the given ID. + /// Also returns any trait-specific additional metadata for the rev reg. async fn get_rev_reg_def_json( &self, rev_reg_id: &RevocationRegistryDefinitionId, - ) -> VcxLedgerResult; + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + Self::RevocationRegistryDefinitionAdditionalMetadata, + )>; + + #[deprecated(note = "use revocation status lists instead")] async fn get_rev_reg_delta_json( &self, rev_reg_id: &RevocationRegistryDefinitionId, from: Option, to: Option, ) -> VcxLedgerResult<(RevocationRegistryDelta, u64)>; + + /// Fetch the revocation status list associated with the ID at the given epoch second + /// `timestamp`. Optionally, metadata from a revocation registry definition fetch can be + /// passed in to optimize required ledger calls. + /// + /// Returns the complete [RevocationStatusList] closest to (at or before) the timestamp, and + /// also returns the actual timestamp that the returned status list entry was made. + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&Self::RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)>; async fn get_rev_reg( &self, rev_reg_id: &RevocationRegistryDefinitionId, @@ -132,6 +161,14 @@ pub trait AnoncredsLedgerWrite: Debug + Send + Sync { ) -> VcxLedgerResult<()>; } +/// Simple utility trait to determine whether the implementor can support reading/writing +/// the specific identifier types. +pub trait AnoncredsLedgerSupport { + fn supports_schema(&self, id: &SchemaId) -> bool; + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool; + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool; +} + pub trait TaaConfigurator: Debug + Send + Sync { fn set_txn_author_agreement_options( &self, diff --git a/aries/aries_vcx_ledger/src/ledger/cheqd/mod.rs b/aries/aries_vcx_ledger/src/ledger/cheqd/mod.rs new file mode 100644 index 0000000000..f7480d80f6 --- /dev/null +++ b/aries/aries_vcx_ledger/src/ledger/cheqd/mod.rs @@ -0,0 +1,368 @@ +use std::{fmt::Debug, sync::Arc}; + +use anoncreds_types::data_types::{ + identifiers::{ + cred_def_id::CredentialDefinitionId, issuer_id::IssuerId, + rev_reg_def_id::RevocationRegistryDefinitionId, schema_id::SchemaId, + }, + ledger::{ + cred_def::CredentialDefinition, + rev_reg::RevocationRegistry, + rev_reg_def::RevocationRegistryDefinition, + rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, + schema::{AttributeNames, Schema}, + }, +}; +use async_trait::async_trait; +use chrono::DateTime; +use did_cheqd::resolution::resolver::DidCheqdResolver; +use did_parser_nom::{Did, DidUrl}; +use did_resolver::shared_types::did_resource::{DidResource, DidResourceMetadata}; +use models::{ + CheqdAnoncredsCredentialDefinition, CheqdAnoncredsRevocationRegistryDefinition, + CheqdAnoncredsRevocationStatusList, CheqdAnoncredsSchema, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerSupport}; +use crate::errors::error::{VcxLedgerError, VcxLedgerResult}; + +mod models; + +const SCHEMA_RESOURCE_TYPE: &str = "anonCredsSchema"; +const CRED_DEF_RESOURCE_TYPE: &str = "anonCredsCredDef"; +const REV_REG_DEF_RESOURCE_TYPE: &str = "anonCredsRevocRegDef"; +const STATUS_LIST_RESOURCE_TYPE: &str = "anonCredsStatusList"; + +/// Struct for resolving anoncreds objects from cheqd ledgers using the cheqd +/// anoncreds object method: https://docs.cheqd.io/product/advanced/anoncreds. +/// +/// Relies on a cheqd DID resolver ([DidCheqdResolver]) to fetch DID resources. +pub struct CheqdAnoncredsLedgerRead { + resolver: Arc, +} + +impl CheqdAnoncredsLedgerRead { + pub fn new(resolver: Arc) -> Self { + Self { resolver } + } + + fn check_resource_type(&self, resource: &DidResource, expected: &str) -> VcxLedgerResult<()> { + let rtyp = &resource.metadata.resource_type; + if rtyp != expected { + return Err(VcxLedgerError::InvalidLedgerResponse(format!( + "Returned resource is not expected type. Got {rtyp}, expected: {expected}" + ))); + } + Ok(()) + } + + async fn get_rev_reg_def_with_resource_metadata( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + ) -> VcxLedgerResult<(RevocationRegistryDefinition, DidResourceMetadata)> { + let url = DidUrl::parse(rev_reg_id.to_string())?; + let resource = self.resolver.resolve_resource(&url).await?; + self.check_resource_type(&resource, REV_REG_DEF_RESOURCE_TYPE)?; + + let data: CheqdAnoncredsRevocationRegistryDefinition = + serde_json::from_slice(&resource.content)?; + Ok(( + RevocationRegistryDefinition { + id: rev_reg_id.to_owned(), + revoc_def_type: data.revoc_def_type, + tag: data.tag, + cred_def_id: data.cred_def_id, + value: data.value, + issuer_id: extract_issuer_id(&url)?, + }, + resource.metadata, + )) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RevocationRegistryDefinitionAdditionalMetadata { + pub resource_name: String, +} + +#[async_trait] +impl AnoncredsLedgerRead for CheqdAnoncredsLedgerRead { + type RevocationRegistryDefinitionAdditionalMetadata = + RevocationRegistryDefinitionAdditionalMetadata; + + async fn get_schema(&self, schema_id: &SchemaId, _: Option<&Did>) -> VcxLedgerResult { + let url = DidUrl::parse(schema_id.to_string())?; + let resource = self.resolver.resolve_resource(&url).await?; + self.check_resource_type(&resource, SCHEMA_RESOURCE_TYPE)?; + + let data: CheqdAnoncredsSchema = serde_json::from_slice(&resource.content)?; + Ok(Schema { + id: schema_id.to_owned(), + seq_no: None, + name: data.name, + version: data.version, + attr_names: AttributeNames(data.attr_names), + issuer_id: extract_issuer_id(&url)?, + }) + } + + async fn get_cred_def( + &self, + cred_def_id: &CredentialDefinitionId, + _: Option<&Did>, + ) -> VcxLedgerResult { + let url = DidUrl::parse(cred_def_id.to_string())?; + let resource = self.resolver.resolve_resource(&url).await?; + self.check_resource_type(&resource, CRED_DEF_RESOURCE_TYPE)?; + + let data: CheqdAnoncredsCredentialDefinition = serde_json::from_slice(&resource.content)?; + Ok(CredentialDefinition { + id: cred_def_id.to_owned(), + schema_id: data.schema_id, + signature_type: data.signature_type, + tag: data.tag, + value: data.value, + issuer_id: extract_issuer_id(&url)?, + }) + } + + async fn get_rev_reg_def_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + RevocationRegistryDefinitionAdditionalMetadata, + )> { + let (rev_reg_def, resource_meta) = self + .get_rev_reg_def_with_resource_metadata(rev_reg_id) + .await?; + + let meta = RevocationRegistryDefinitionAdditionalMetadata { + resource_name: resource_meta.resource_name, + }; + + Ok((rev_reg_def, meta)) + } + + async fn get_rev_reg_delta_json( + &self, + _rev_reg_id: &RevocationRegistryDefinitionId, + _from: Option, + _to: Option, + ) -> VcxLedgerResult<(RevocationRegistryDelta, u64)> { + // unsupported, to be removed: https://github.com/hyperledger/aries-vcx/issues/1309 + Err(VcxLedgerError::UnimplementedFeature( + "get_rev_reg_delta_json not supported for cheqd".into(), + )) + } + + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + let rev_reg_def_url = DidUrl::parse(rev_reg_id.to_string())?; + + // refetch if needed + let rev_reg_def_meta = match rev_reg_def_meta { + Some(v) => v, + None => &self.get_rev_reg_def_json(rev_reg_id).await?.1, + }; + + //credo-ts uses the metadata.name or fails (https://docs.cheqd.io/product/advanced/anoncreds/revocation-status-list#same-resource-name-different-resource-type) + let name = &rev_reg_def_meta.resource_name; + + let did = rev_reg_def_url + .did() + .ok_or(VcxLedgerError::InvalidInput(format!( + "DID URL missing DID {rev_reg_def_url}" + )))?; + + let resource_dt = + DateTime::from_timestamp(timestamp as i64, 0).ok_or(VcxLedgerError::InvalidInput( + format!("input status list timestamp is not valid {timestamp}"), + ))?; + + // assemble query + let xml_dt = resource_dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + let mut query = Url::parse(did) + .map_err(|e| VcxLedgerError::InvalidInput(format!("cannot parse DID as URL: {e}")))?; + query + .query_pairs_mut() + .append_pair("resourceType", STATUS_LIST_RESOURCE_TYPE) + .append_pair("resourceName", name) + .append_pair("resourceVersionTime", &xml_dt); + let query_url = DidUrl::parse(query.to_string())?; + + let resource = self.resolver.resolve_resource(&query_url).await?; + self.check_resource_type(&resource, STATUS_LIST_RESOURCE_TYPE)?; + + let data: CheqdAnoncredsRevocationStatusList = serde_json::from_slice(&resource.content)?; + let timestamp = resource.metadata.created.timestamp() as u64; + + let status_list = RevocationStatusList { + rev_reg_def_id: Some(rev_reg_id.to_owned()), + issuer_id: extract_issuer_id(&rev_reg_def_url)?, + revocation_list: data.revocation_list, + accum: data.accum, + timestamp: Some(timestamp), + }; + + Ok((status_list, timestamp)) + } + + async fn get_rev_reg( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + ) -> VcxLedgerResult<(RevocationRegistry, u64)> { + let (list, last_updated) = self + .get_rev_status_list(rev_reg_id, timestamp, None) + .await?; + + let accum = list + .accum + .ok_or(VcxLedgerError::InvalidLedgerResponse(format!( + "response status list is missing an accumulator: {list:?}" + )))?; + + let reg = RevocationRegistry { + value: accum.into(), + }; + + Ok((reg, last_updated)) + } +} + +fn extract_issuer_id(url: &DidUrl) -> VcxLedgerResult { + let did = url.did().ok_or(VcxLedgerError::InvalidInput(format!( + "DID URL is missing a DID: {url}" + )))?; + IssuerId::new(did) + .map_err(|e| VcxLedgerError::InvalidInput(format!("DID is not an IssuerId {e}"))) +} + +impl AnoncredsLedgerSupport for CheqdAnoncredsLedgerRead { + fn supports_schema(&self, id: &SchemaId) -> bool { + let Ok(url) = DidUrl::parse(id.to_string()) else { + return false; + }; + url.method() == Some("cheqd") + } + + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool { + let Ok(url) = DidUrl::parse(id.to_string()) else { + return false; + }; + url.method() == Some("cheqd") + } + + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool { + let Ok(url) = DidUrl::parse(id.to_string()) else { + return false; + }; + url.method() == Some("cheqd") + } +} + +impl Debug for CheqdAnoncredsLedgerRead { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CheqdAnoncredsLedgerRead instance") + } +} + +#[cfg(test)] +mod unit_tests { + use super::*; + + fn default_cheqd_reader() -> CheqdAnoncredsLedgerRead { + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))) + } + + #[test] + fn test_anoncreds_schema_support() { + let reader = default_cheqd_reader(); + + // qualified cheqd + assert!(reader.supports_schema( + &SchemaId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + + // unqualified + assert!(!reader.supports_schema( + &SchemaId::new("7BPMqYgYLQni258J8JPS8K:2:degree schema:46.58.87").unwrap() + )); + // qualified sov + assert!(!reader.supports_schema( + &SchemaId::new("did:sov:7BPMqYgYLQni258J8JPS8K:2:degree schema:46.58.87").unwrap() + )); + } + + #[test] + fn test_anoncreds_cred_def_support() { + let reader = default_cheqd_reader(); + + // qualified cheqd + assert!(reader.supports_credential_definition( + &CredentialDefinitionId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + + // unqualified + assert!(!reader.supports_credential_definition( + &CredentialDefinitionId::new( + "7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.degree_schema" + ) + .unwrap() + )); + // qualified sov + assert!(!reader.supports_credential_definition( + &CredentialDefinitionId::new( + "did:sov:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.degree_schema" + ) + .unwrap() + )); + } + + #[test] + fn test_anoncreds_rev_reg_support() { + let reader = default_cheqd_reader(); + + // qualified cheqd + assert!(reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + + // unqualified + assert!(!reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "7BPMqYgYLQni258J8JPS8K:4:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.\ + degree_schema:CL_ACCUM:61d5a381-30be-4120-9307-b150b49c203c" + ) + .unwrap() + )); + // qualified sov + assert!(!reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "did:sov:7BPMqYgYLQni258J8JPS8K:4:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.\ + degree_schema:CL_ACCUM:61d5a381-30be-4120-9307-b150b49c203c" + ) + .unwrap() + )); + } +} diff --git a/aries/aries_vcx_ledger/src/ledger/cheqd/models.rs b/aries/aries_vcx_ledger/src/ledger/cheqd/models.rs new file mode 100644 index 0000000000..e6a8b7496a --- /dev/null +++ b/aries/aries_vcx_ledger/src/ledger/cheqd/models.rs @@ -0,0 +1,51 @@ +//! Cheqd Ledger data models, derived from https://docs.cheqd.io/product/advanced/anoncreds + +use anoncreds_types::data_types::{ + identifiers::{cred_def_id::CredentialDefinitionId, schema_id::SchemaId}, + ledger::{ + cred_def::{CredentialDefinitionData, SignatureType}, + rev_reg_def::{RegistryType, RevocationRegistryDefinitionValue}, + rev_status_list::serde_revocation_list, + }, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheqdAnoncredsSchema { + pub name: String, + pub version: String, + pub attr_names: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheqdAnoncredsCredentialDefinition { + pub schema_id: SchemaId, + #[serde(rename = "type")] + pub signature_type: SignatureType, + pub tag: String, + pub value: CredentialDefinitionData, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheqdAnoncredsRevocationRegistryDefinition { + pub revoc_def_type: RegistryType, + pub cred_def_id: CredentialDefinitionId, + pub tag: String, + pub value: RevocationRegistryDefinitionValue, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheqdAnoncredsRevocationStatusList { + #[serde(with = "serde_revocation_list")] + pub revocation_list: bitvec::vec::BitVec, + #[serde( + rename = "currentAccumulator", + alias = "accum", + skip_serializing_if = "Option::is_none" + )] + pub accum: Option, +} diff --git a/aries/aries_vcx_ledger/src/ledger/indy_vdr_ledger.rs b/aries/aries_vcx_ledger/src/ledger/indy_vdr_ledger.rs index 2bfbd63141..96350e07f6 100644 --- a/aries/aries_vcx_ledger/src/ledger/indy_vdr_ledger.rs +++ b/aries/aries_vcx_ledger/src/ledger/indy_vdr_ledger.rs @@ -4,16 +4,19 @@ use std::{ sync::RwLock, }; -use anoncreds_types::data_types::{ - identifiers::{ - cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, - schema_id::SchemaId, - }, - ledger::{ - cred_def::CredentialDefinition, rev_reg::RevocationRegistry, - rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, - schema::Schema, +use anoncreds_types::{ + data_types::{ + identifiers::{ + cred_def_id::CredentialDefinitionId, issuer_id::IssuerId, + rev_reg_def_id::RevocationRegistryDefinitionId, schema_id::SchemaId, + }, + ledger::{ + cred_def::CredentialDefinition, rev_reg::RevocationRegistry, + rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, schema::Schema, + }, }, + utils::conversions::from_revocation_registry_delta_to_revocation_status_list, }; use aries_vcx_wallet::wallet::base_wallet::BaseWallet; use async_trait::async_trait; @@ -32,6 +35,7 @@ use indy_vdr::{ }; use log::{debug, trace}; use public_key::Key; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use time::OffsetDateTime; use vdr::{ @@ -52,7 +56,10 @@ pub use vdr::{ }; use super::{ - base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerWrite, IndyLedgerRead, IndyLedgerWrite}, + base_ledger::{ + AnoncredsLedgerRead, AnoncredsLedgerSupport, AnoncredsLedgerWrite, IndyLedgerRead, + IndyLedgerWrite, + }, map_error_not_found_to_none, request_submitter::{ vdr_ledger::{IndyVdrLedgerPool, IndyVdrSubmitter}, @@ -182,8 +189,11 @@ where // mimic acapy & credo-ts behaviour - assumes node protocol >= 1.4 // check correct tx - if txn["type"] != json!("101") { - return Err(VcxLedgerError::InvalidLedgerResponse); + let txn_type = &txn["type"]; + if txn_type != &json!("101") { + return Err(VcxLedgerError::InvalidLedgerResponse(format!( + "Expected indy schema transaction type (101), found: {txn_type}" + ))); } // pull schema identifier parts @@ -193,7 +203,9 @@ where let (Value::String(did), Value::String(name), Value::String(ver)) = (schema_did, schema_name, schema_version) else { - return Err(VcxLedgerError::InvalidLedgerResponse); + return Err(VcxLedgerError::InvalidLedgerResponse( + "Could not resolve schema DID, name & version from txn".into(), + )); }; // construct indy schema ID from parts @@ -483,12 +495,21 @@ where } } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RevocationRegistryDefinitionAdditionalMetadata { + pub max_cred_num: usize, + pub issuer_id: IssuerId, +} + #[async_trait] impl AnoncredsLedgerRead for IndyVdrLedgerRead where T: RequestSubmitter + Send + Sync, V: ResponseCacher + Send + Sync, { + type RevocationRegistryDefinitionAdditionalMetadata = + RevocationRegistryDefinitionAdditionalMetadata; + async fn get_schema( &self, schema_id: &SchemaId, @@ -545,7 +566,10 @@ where async fn get_rev_reg_def_json( &self, rev_reg_id: &RevocationRegistryDefinitionId, - ) -> VcxLedgerResult { + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + RevocationRegistryDefinitionAdditionalMetadata, + )> { debug!("get_rev_reg_def_json >> rev_reg_id: {rev_reg_id}"); let id = RevocationRegistryId::from_str(&rev_reg_id.to_string())?; let request = self @@ -558,7 +582,14 @@ where let rev_reg_def = self .response_parser .parse_get_revoc_reg_def_response(&response)?; - Ok(rev_reg_def.convert(())?) + let def = rev_reg_def.convert(())?; + + let meta = RevocationRegistryDefinitionAdditionalMetadata { + max_cred_num: def.value.max_cred_num as usize, + issuer_id: def.issuer_id.clone(), + }; + + Ok((def, meta)) } async fn get_rev_reg_delta_json( @@ -593,6 +624,38 @@ where Ok((revoc_reg_delta.convert(())?, timestamp)) } + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 + let (delta, entry_time) = self + .get_rev_reg_delta_json(rev_reg_id, Some(0), Some(timestamp)) + .await?; + + let rev_reg_def_meta = match rev_reg_def_meta { + Some(x) => x, + None => &self.get_rev_reg_def_json(rev_reg_id).await?.1, + }; + + let status_list = from_revocation_registry_delta_to_revocation_status_list( + &delta.value, + entry_time, + rev_reg_id, + rev_reg_def_meta.max_cred_num, + rev_reg_def_meta.issuer_id.clone(), + ) + .map_err(|e| { + VcxLedgerError::InvalidLedgerResponse(format!( + "received rev status delta could not be translated to status list: {e} {delta:?}" + )) + })?; + + Ok((status_list, entry_time)) + } + async fn get_rev_reg( &self, rev_reg_id: &RevocationRegistryDefinitionId, @@ -621,6 +684,41 @@ where } } +impl AnoncredsLedgerSupport for IndyVdrLedgerRead { + fn supports_schema(&self, id: &SchemaId) -> bool { + if id.is_legacy() { + // unqualified + return true; + } + did_method_is_supported(&id.0) + } + + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool { + if id.is_legacy_cred_def_identifier() { + // unqualified + return true; + } + did_method_is_supported(&id.0) + } + + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool { + if id.is_legacy() { + // unqualified + return true; + } + did_method_is_supported(&id.0) + } +} + +fn did_method_is_supported(id: &str) -> bool { + let is_sov = id.starts_with("did:sov:"); + let is_unqualified = !id.starts_with("did"); + + // FUTURE - indy & namespace + + is_sov || is_unqualified +} + #[async_trait] impl AnoncredsLedgerWrite for IndyVdrLedgerWrite where @@ -648,7 +746,7 @@ where .sign_and_submit_request(wallet, submitter_did, request) .await; - if let Err(VcxLedgerError::InvalidLedgerResponse) = &sign_result { + if matches!(sign_result, Err(VcxLedgerError::InvalidLedgerResponse(_))) { return Err(VcxLedgerError::DuplicationSchema); } sign_result.map(|_| ()) @@ -764,3 +862,108 @@ pub fn build_ledger_components( Ok((ledger_read, ledger_write)) } + +#[cfg(test)] +mod unit_tests { + use mockall::mock; + + use super::*; + use crate::ledger::response_cacher::noop::NoopResponseCacher; + + mock! { + pub RequestSubmitter {} + #[async_trait] + impl RequestSubmitter for RequestSubmitter { + async fn submit(&self, request: indy_vdr::pool::PreparedRequest) -> VcxLedgerResult; + } + } + + fn dummy_indy_vdr_reader() -> IndyVdrLedgerRead { + IndyVdrLedgerRead::new(IndyVdrLedgerReadConfig { + request_submitter: MockRequestSubmitter::new(), + response_parser: indy_ledger_response_parser::ResponseParser, + response_cacher: NoopResponseCacher, + protocol_version: ProtocolVersion::Node1_4, + }) + } + + #[test] + fn test_anoncreds_schema_support() { + let reader = dummy_indy_vdr_reader(); + + // legacy + assert!(reader.supports_schema( + &SchemaId::new("7BPMqYgYLQni258J8JPS8K:2:degree schema:46.58.87").unwrap() + )); + // qualified sov + assert!(reader.supports_schema( + &SchemaId::new("did:sov:7BPMqYgYLQni258J8JPS8K:2:degree schema:46.58.87").unwrap() + )); + // qualified cheqd + assert!(!reader.supports_schema( + &SchemaId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + } + + #[test] + fn test_anoncreds_cred_def_support() { + let reader = dummy_indy_vdr_reader(); + + // legacy + assert!(reader.supports_credential_definition( + &CredentialDefinitionId::new( + "7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.degree_schema" + ) + .unwrap() + )); + // qualified sov + assert!(reader.supports_credential_definition( + &CredentialDefinitionId::new( + "did:sov:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.degree_schema" + ) + .unwrap() + )); + // qualified cheqd + assert!(!reader.supports_credential_definition( + &CredentialDefinitionId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + } + + #[test] + fn test_anoncreds_rev_reg_support() { + let reader = dummy_indy_vdr_reader(); + + // legacy + assert!(reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "7BPMqYgYLQni258J8JPS8K:4:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.\ + degree_schema:CL_ACCUM:61d5a381-30be-4120-9307-b150b49c203c" + ) + .unwrap() + )); + // qualified sov + assert!(reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "did:sov:7BPMqYgYLQni258J8JPS8K:4:7BPMqYgYLQni258J8JPS8K:3:CL:70:faber.agent.\ + degree_schema:CL_ACCUM:61d5a381-30be-4120-9307-b150b49c203c" + ) + .unwrap() + )); + // qualified cheqd + assert!(!reader.supports_revocation_registry( + &RevocationRegistryDefinitionId::new( + "did:cheqd:mainnet:7BPMqYgYLQni258J8JPS8K/resources/\ + 6259d357-eeb1-4b98-8bee-12a8390d3497" + ) + .unwrap() + )); + } +} diff --git a/aries/aries_vcx_ledger/src/ledger/mod.rs b/aries/aries_vcx_ledger/src/ledger/mod.rs index 32d9c595ad..3a80c32684 100644 --- a/aries/aries_vcx_ledger/src/ledger/mod.rs +++ b/aries/aries_vcx_ledger/src/ledger/mod.rs @@ -1,10 +1,14 @@ use crate::errors::error::VcxLedgerError; +pub mod arc; pub mod base_ledger; pub mod common; +#[cfg(feature = "cheqd")] +pub mod cheqd; pub mod indy; pub mod indy_vdr_ledger; +pub mod multi_ledger; mod type_conversion; pub mod request_submitter; diff --git a/aries/aries_vcx_ledger/src/ledger/multi_ledger.rs b/aries/aries_vcx_ledger/src/ledger/multi_ledger.rs new file mode 100644 index 0000000000..fa00035e5b --- /dev/null +++ b/aries/aries_vcx_ledger/src/ledger/multi_ledger.rs @@ -0,0 +1,685 @@ +use std::fmt::Debug; + +use anoncreds_types::data_types::{ + identifiers::{ + cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, + schema_id::SchemaId, + }, + ledger::{ + cred_def::CredentialDefinition, rev_reg::RevocationRegistry, + rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, schema::Schema, + }, +}; +use async_trait::async_trait; +use did_parser_nom::Did; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use super::base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerSupport}; +use crate::errors::error::{VcxLedgerError, VcxLedgerResult}; + +// FUTURE - this multi-ledger anoncreds reader finds the first impl that supports the identifier +// and attempts to use it. This behaviour may need some enhancements in the future when +// considering coordination of unqualified object resolution, when multiple capable anoncreds +// readers are available (e.g. sovrin testnet & sovrin mainnet). +// Enhancements may include: +// * priority system - try A resolver before B if A & B both support the identifier +// * fallback/chain system - try A resolver, if it fails, try B resolver +// Alternatively these enhancements can be skipped if qualified DIDs/objects are used instead, +// e.g. did:indy:a:123, did:indy:b:123 + +/// Struct to aggregate multiple [AnoncredsLedgerRead] implementations into a single +/// [AnoncredsLedgerRead]. The child [AnoncredsLedgerRead] implementations are +/// utilized depending on whether or not they support resolution of the given object ID +/// (e.g. based on the DID Method). +#[derive(Default, Debug)] +pub struct MultiLedgerAnoncredsRead { + readers: Vec>, +} + +#[async_trait] +impl AnoncredsLedgerRead for MultiLedgerAnoncredsRead { + type RevocationRegistryDefinitionAdditionalMetadata = Value; + + async fn get_schema( + &self, + schema_id: &SchemaId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + let reader = self + .readers + .iter() + .find(|r| r.supports_schema(schema_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + schema_id.to_string(), + ))?; + + reader.get_schema(schema_id, submitter_did).await + } + + async fn get_cred_def( + &self, + cred_def_id: &CredentialDefinitionId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + let reader = self + .readers + .iter() + .find(|r| r.supports_credential_definition(cred_def_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + cred_def_id.to_string(), + ))?; + + reader.get_cred_def(cred_def_id, submitter_did).await + } + + async fn get_rev_reg_def_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + Self::RevocationRegistryDefinitionAdditionalMetadata, + )> { + let reader = self + .readers + .iter() + .find(|r| r.supports_revocation_registry(rev_reg_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + rev_reg_id.to_string(), + ))?; + + reader.get_rev_reg_def_json(rev_reg_id).await + } + async fn get_rev_reg_delta_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + from: Option, + to: Option, + ) -> VcxLedgerResult<(RevocationRegistryDelta, u64)> { + let reader = self + .readers + .iter() + .find(|r| r.supports_revocation_registry(rev_reg_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + rev_reg_id.to_string(), + ))?; + + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 + reader.get_rev_reg_delta_json(rev_reg_id, from, to).await + } + + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&Self::RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + let reader = self + .readers + .iter() + .find(|r| r.supports_revocation_registry(rev_reg_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + rev_reg_id.to_string(), + ))?; + + reader + .get_rev_status_list(rev_reg_id, timestamp, rev_reg_def_meta) + .await + } + + async fn get_rev_reg( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + ) -> VcxLedgerResult<(RevocationRegistry, u64)> { + let reader = self + .readers + .iter() + .find(|r| r.supports_revocation_registry(rev_reg_id)) + .ok_or(VcxLedgerError::UnsupportedLedgerIdentifier( + rev_reg_id.to_string(), + ))?; + + reader.get_rev_reg(rev_reg_id, timestamp).await + } +} + +impl MultiLedgerAnoncredsRead { + pub fn new() -> Self { + Self::default() + } + + pub fn register_reader(mut self, reader: T) -> Self + where + T: AnoncredsLedgerRead + AnoncredsLedgerSupport + 'static, + for<'de> ::RevocationRegistryDefinitionAdditionalMetadata: + Serialize + Deserialize<'de> + Send + Sync, + { + let adaptor = AnoncredsLedgerReadAdaptor { inner: reader }; + self.readers.push(Box::new(adaptor)); + self + } +} + +impl AnoncredsLedgerSupport for MultiLedgerAnoncredsRead { + fn supports_schema(&self, id: &SchemaId) -> bool { + self.readers.iter().any(|r| r.supports_schema(id)) + } + + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool { + self.readers + .iter() + .any(|r| r.supports_credential_definition(id)) + } + + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool { + self.readers + .iter() + .any(|r| r.supports_revocation_registry(id)) + } +} + +#[derive(Debug)] +pub struct AnoncredsLedgerReadAdaptor { + inner: T, +} + +pub trait AnoncredsLedgerReadAdaptorTrait: + AnoncredsLedgerRead + AnoncredsLedgerSupport +{ +} + +impl AnoncredsLedgerSupport for AnoncredsLedgerReadAdaptor +where + T: AnoncredsLedgerSupport, +{ + fn supports_schema(&self, id: &SchemaId) -> bool { + self.inner.supports_schema(id) + } + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool { + self.inner.supports_credential_definition(id) + } + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool { + self.inner.supports_revocation_registry(id) + } +} + +#[async_trait] +impl AnoncredsLedgerRead for AnoncredsLedgerReadAdaptor +where + T: AnoncredsLedgerRead, + T::RevocationRegistryDefinitionAdditionalMetadata: + Serialize + for<'de> Deserialize<'de> + Send + Sync, +{ + type RevocationRegistryDefinitionAdditionalMetadata = Value; + + async fn get_schema( + &self, + schema_id: &SchemaId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + self.inner.get_schema(schema_id, submitter_did).await + } + + async fn get_cred_def( + &self, + cred_def_id: &CredentialDefinitionId, + submitter_did: Option<&Did>, + ) -> VcxLedgerResult { + self.inner.get_cred_def(cred_def_id, submitter_did).await + } + + async fn get_rev_reg_def_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + ) -> VcxLedgerResult<( + RevocationRegistryDefinition, + Self::RevocationRegistryDefinitionAdditionalMetadata, + )> { + let (reg, meta) = self.inner.get_rev_reg_def_json(rev_reg_id).await?; + + Ok((reg, serde_json::to_value(meta)?)) + } + async fn get_rev_reg_delta_json( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + from: Option, + to: Option, + ) -> VcxLedgerResult<(RevocationRegistryDelta, u64)> { + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 + self.inner + .get_rev_reg_delta_json(rev_reg_id, from, to) + .await + } + + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + rev_reg_def_meta: Option<&Self::RevocationRegistryDefinitionAdditionalMetadata>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + let meta = match rev_reg_def_meta { + Some(v) => Some(serde_json::from_value(v.to_owned())?), + None => None, + }; + + self.inner + .get_rev_status_list(rev_reg_id, timestamp, meta.as_ref()) + .await + } + async fn get_rev_reg( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + ) -> VcxLedgerResult<(RevocationRegistry, u64)> { + self.inner.get_rev_reg(rev_reg_id, timestamp).await + } +} + +impl AnoncredsLedgerReadAdaptorTrait for AnoncredsLedgerReadAdaptor +where + T: AnoncredsLedgerRead + AnoncredsLedgerSupport, + T::RevocationRegistryDefinitionAdditionalMetadata: + Serialize + for<'de> Deserialize<'de> + Send + Sync, +{ +} + +#[cfg(test)] +mod unit_tests { + use async_trait::async_trait; + use mockall::{mock, predicate::eq}; + use serde_json::json; + + use super::*; + + mock! { + #[derive(Debug)] + pub Reader {} + #[async_trait] + impl AnoncredsLedgerRead for Reader { + type RevocationRegistryDefinitionAdditionalMetadata = Value; + + // NOTE: these method signatures were generated as a result of the expanded #[async_trait] form. + // this was needed to escape some #[async_trait] compiling issues + fn get_schema<'life0,'life1,'life2,'async_trait>(&'life0 self,schema_id: &'life1 SchemaId,submitter_did:Option< &'life2 Did> ,) -> ::core::pin::Pin > + ::core::marker::Send+'async_trait> >where 'life0:'async_trait,'life1:'async_trait,'life2:'async_trait,Self:'async_trait; + fn get_cred_def<'life0,'life1,'life2,'async_trait>(&'life0 self,cred_def_id: &'life1 CredentialDefinitionId,submitter_did:Option< &'life2 Did> ,) -> ::core::pin::Pin > + ::core::marker::Send+'async_trait> >where 'life0:'async_trait,'life1:'async_trait,'life2:'async_trait,Self:'async_trait; + async fn get_rev_reg_def_json(&self, rev_reg_id: &RevocationRegistryDefinitionId) -> VcxLedgerResult<(RevocationRegistryDefinition, Value)>; + async fn get_rev_reg_delta_json(&self, rev_reg_id: &RevocationRegistryDefinitionId, from: Option, to: Option) -> VcxLedgerResult<(RevocationRegistryDelta, u64)>; + #[allow(clippy::type_complexity)] // generated + fn get_rev_status_list<'life0,'life1,'life2,'async_trait>(&'life0 self,rev_reg_id: &'life1 RevocationRegistryDefinitionId,timestamp:u64,rev_reg_def_meta:Option< &'life2 Value>) -> ::core::pin::Pin > + ::core::marker::Send+'async_trait> >where 'life0:'async_trait,'life1:'async_trait,'life2:'async_trait,Self:'async_trait; + async fn get_rev_reg(&self, rev_reg_id: &RevocationRegistryDefinitionId, timestamp: u64) -> VcxLedgerResult<(RevocationRegistry, u64)>; + } + impl AnoncredsLedgerSupport for Reader { + fn supports_schema(&self, id: &SchemaId) -> bool; + fn supports_credential_definition(&self, id: &CredentialDefinitionId) -> bool; + fn supports_revocation_registry(&self, id: &RevocationRegistryDefinitionId) -> bool; + } + } + + #[test] + fn test_anoncreds_supports_schema_if_any_support() { + let mut reader1 = MockReader::new(); + reader1.expect_supports_schema().return_const(false); + let mut reader2 = MockReader::new(); + reader2.expect_supports_schema().return_const(false); + let mut reader3 = MockReader::new(); + reader3.expect_supports_schema().return_const(true); + + // no readers + let mut reader = MultiLedgerAnoncredsRead::new(); + assert!(!reader.supports_schema(&SchemaId::new_unchecked(""))); + + // with reader 1 + reader = reader.register_reader(reader1); + assert!(!reader.supports_schema(&SchemaId::new_unchecked(""))); + + // with reader 1,2 + reader = reader.register_reader(reader2); + assert!(!reader.supports_schema(&SchemaId::new_unchecked(""))); + + // with reader 1,2,3 + reader = reader.register_reader(reader3); + assert!(reader.supports_schema(&SchemaId::new_unchecked(""))); + } + + #[test] + fn test_anoncreds_supports_cred_def_if_any_support() { + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_credential_definition() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_credential_definition() + .return_const(false); + let mut reader3 = MockReader::new(); + reader3 + .expect_supports_credential_definition() + .return_const(true); + + // no readers + let mut reader = MultiLedgerAnoncredsRead::new(); + assert!(!reader.supports_credential_definition(&CredentialDefinitionId::new_unchecked(""))); + + // with reader 1 + reader = reader.register_reader(reader1); + assert!(!reader.supports_credential_definition(&CredentialDefinitionId::new_unchecked(""))); + + // with reader 1,2 + reader = reader.register_reader(reader2); + assert!(!reader.supports_credential_definition(&CredentialDefinitionId::new_unchecked(""))); + + // with reader 1,2,3 + reader = reader.register_reader(reader3); + assert!(reader.supports_credential_definition(&CredentialDefinitionId::new_unchecked(""))); + } + + #[test] + fn test_anoncreds_supports_rev_reg_if_any_support() { + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader3 = MockReader::new(); + reader3 + .expect_supports_revocation_registry() + .return_const(true); + + // no readers + let mut reader = MultiLedgerAnoncredsRead::new(); + assert!(!reader + .supports_revocation_registry(&RevocationRegistryDefinitionId::new_unchecked(""))); + + // with reader 1 + reader = reader.register_reader(reader1); + assert!(!reader + .supports_revocation_registry(&RevocationRegistryDefinitionId::new_unchecked(""))); + + // with reader 1,2 + reader = reader.register_reader(reader2); + assert!(!reader + .supports_revocation_registry(&RevocationRegistryDefinitionId::new_unchecked(""))); + + // with reader 1,2,3 + reader = reader.register_reader(reader3); + assert!( + reader.supports_revocation_registry(&RevocationRegistryDefinitionId::new_unchecked("")) + ); + } + + #[tokio::test] + async fn test_get_schema_proxy() { + let id = SchemaId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let schema: Schema = serde_json::from_value(json!({ + "id": "2hoqvcwupRTUNkXn6ArYzs:2:test-licence:4.4.4", + "issuerId": "https://example.org/issuers/74acabe2-0edc-415e-ad3d-c259bac04c15", + "name": "Example schema", + "version": "0.0.1", + "attrNames": ["name", "age", "vmax"] + })) + .unwrap(); + + let mut reader1 = MockReader::new(); + reader1.expect_supports_schema().return_const(false); + let mut reader2 = MockReader::new(); + reader2.expect_supports_schema().return_const(true); + + let return_schema = schema.clone(); + let expected_id = id.clone(); + reader2 + .expect_get_schema() + .times(1) + .withf(move |id, _| id == &expected_id) + .return_once(move |_, _| { + let schema = return_schema.clone(); + Box::pin(async { Ok(schema) }) + }); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let actual_schema = reader.get_schema(&id, None).await.unwrap(); + assert_eq!(actual_schema, schema); + } + + #[tokio::test] + async fn test_get_cred_def_proxy() { + let id = CredentialDefinitionId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let cred_def: CredentialDefinition = serde_json::from_value(json!({ + "issuerId":"2hoqvcwupRTUNkXn6ArYzs", + "id":"V4SGRU86Z58d6TV7PBUe6f:3:CL:47:tag1", + "schemaId":"47", + "type":"CL", + "tag":"tag1", + "value":{"primary":{"n":"84315068910733942941538809865498212264459549000792879639194636554256493686411316918356364554212859153177297904254803226691261360163017568975126644984686667408908554649062870983454913573717132685671583694031717814141080679830350673819424952039249083306114176943232365022146274429759329160659813641353420711789491657407711667921406386914492317126931232096716361835185621871614892278526459017279649489759883446875710292582916862323180484745724279188511139859961558899492420881308319897939707149225665665750089691347075452237689762438550797003970061912118059748053097698671091262939106899007559343210815224380894488995113","s":"59945496099091529683010866609186265795684084593915879677365915314463441987443154552461559065352420479185095999059693986296256981985940684025427266913415378231498151267007158809467729823827861060943249985214295565954619712884813475078876243461567805408186530126430117091175878433493470477405243290350006589531746383498912735958116529589160259765518623375993634632483497463706818065357079852866706817198531892216731574724567574484572625164896490617904130456390333434781688685711531389963108606060826034467848677030185954836450146848456029587966315982491265726825962242361193382187200762212532433623355158921675478443655","r":{"date":"4530952538742033870264476947349521164360569832293598526413037147348444667070532579011380195228953804587892910083371419619205239227176615305655903664544843027114068926732016634797988559479373173420379032359085866159812396874183493337593923986352373367443489125306598512961937690050408637316178808308468698130916843726350286470498112647012795872536655274223930769557053638433983185163528151238000191689201476902211980373037910868763273992732059619082351273391415938130689371576452787378432526477108387174464183508295202268819673712009207498503854885022563424081101176507778361451123464434452487512804710607807387159128","age":"15503562420190113779412573443648670692955804417106499721072073114914016752313409982751974380400397358007517489487316569306402072592113120841927939614225596950574350717260042735712458756920802142037678828871321635736672163943850608100354921834523463955560137056697629609048487954091228011758465160630168342803951659774432090950114225268433943738713511960512519720523823132697152235977167573478225681125743108316237901888395175219199986619980202460002105538052202194307901021813863765169429019328213794814861353730831662815923471654084390063142965516688500592978949402225958824367010905689069418109693434050714606537583","master_secret":"14941959991861844203640254789113219741444241402376608919648803136983822447657869566295932734574583636024573117873598134469823095273660720727015649700465955361130129864938450014649937111357170711555934174503723640116145690157792143484339964157425981213977310483344564302214614951542098664609872435210449645226834832312148045998264085006307562873923691290268448463268834055489643805348568651181211925383052438130996893167066397253030164424601706641019876113890399331369874738474583032456630131756536716380809815371596958967704560484978381009830921031570414600773593753852611696648360844881738828836829723309887344258937","degree":"67311842668657630299931187112088054454211766880915366228670112262543717421860411017794917555864962789983330613927578732076546462151555711446970436129702520726176833537897538147071443776547628756352432432835899834656529545556299956904072273738406120215054506535933414063527222201017224487746551625599741110865905908376807007226016794285559868443574843566769079505217003255193711592800832900528743423878219697996298053773029816576222817010079313631823474213756143038164999157352990720891769630050634864109412199796030812795554794690666251581638175258059399898533412136730015599390155694930639754604259112806273788686507","name":"40219920836667226846980595968749841377080664270433982561850197901597771608457272395564987365558822976616985553706043669221173208652485993987057610703687751081160705282278075965587059598006922553243119778614657332904443210949022832267152413602769950017196241603290054807405460837019722951966510572795690156337466164027728801433988397914462240536123226066351251559900540164628048512242681757179461134506191038056117474598282470153495944204106741140156797129717000379741035610651103388047392025454843123388667794650989667119908702980140953817053781064048661940525040691050449058181790166453523432033421772638164421440685"},"rctxt":"82211384518226913414705883468527183492640036595481131419387459490531641219140075943570487048949702495964452189878216535351106872901004288800618738558792099255633906919332859771231971995964089510050225371749467514963769062274472125808020747444043470829002911273221886787637565057381007986495929400181408859710441856354570835196999403016572481694010972193591132652075787983469256801030377358546763251500115728919042442285672299538383497028946509014399210152856456151424115321673276206158701693427269192935107281015760959764697283756967072538997471422184132520456634304294669685041869025373789168601981777301930702142717","z":"597630352445077967631363625241310742422278192985165396805410934637833378871905757950406233560527227666603560941363695352019306999881059381981398160064772044355803827657704204341958693819520622597159046224481967205282164000142449330594352761829061889177257816365267888924702641013893644925126158726833651215929952524830180400844722456288706452674387695005404773156271263201912761375013255052598817079064887267420056304601888709527771529363399401864219168554878211931445812982845627741042319366707757767391990264005995464401655063121998656818808854159318505715730088491626077711079755220554530286586303737320102790443"},"revocation":{"g":"1 163FAAD4EB9AA0DF02C19EAC9E91DAFF5C9EEC50B13D59613BB03AC57A864724 1 091121B3A92D2C48A12FB2B6904AFDA9708EAC9CBDF4E9BF988C9071BB4CFEC2 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","g_dash":"1 165E30BED89E08D23FC61685E5F38A65A74342EDF75283BE2E3D7A84D036AC1F 1 07D4894035E05420317656B39B0104E2E6CF372024C8DA9B5E69C05D073DEE17 1 2408458D7F1B790AC27A05055FB7DB562BD51E2597BC3CA5713589716A128647 1 1965D421EA07B38C9C287348BC6AAC53B7FF6E44DE6AC3202F4E62B147019FB3 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000","h":"1 1EC742963B128F781DEC49BF60E9D7D627BE75EE6DB6FC7EC0A4388EB6EDDD5E 1 0A98F72733982BF22B83E40FB03AA339C990424498DFF7D227B75F442F089E71 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","h0":"1 1EBE1D3B82473D8435B13E1B22B9B8A3FFD8251F3FACF423CE3CF0A63AF81D6B 1 10890876E36CCB96308ED4C284CDC4B2B014AE67404207E73F287EC86ACFE809 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","h1":"1 21F6A9DA5A812DB4840340A817788CC84EB3C079E07C9908E9637C0A00F2DD56 1 1B1A0005E895B479500E818FC2280B6D06C088788CCF44C07E94B55941EE85F6 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","h2":"1 180ADD04BFF577BCC49D09F97A9C11347C7A0359A0347DE9F138393CAF5F1F93 1 1044FFDF4AC72BBD8B6CC38D918A7C64A441E53D4561A7F5799B68D48E355294 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","htilde":"1 031D6DDE2A7B05F29EFB574973E6D54AE36B79EBDD0599CD5AD2DF93BDBD0661 1 23A358FEC4883CE1EF44EEC1640B4D4C27FF5C7D64E9798BBF2C5A0D414D1AB5 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","h_cap":"1 1536F787B8F6676E31B769543085CC12E484D6B9A136A29E05723DE993E52C78 1 05EF3C2E5AC1F62132E1F62AC715588203902BCBA8D40203606E6834F9065BB5 1 09878859092CA40C7D5AB4D42F6AFC16987CC90C361F161F9383BCD70F0BD7F0 1 2472E732278D393032B33DEDD2F38F84C3D05E109819E97D462D55822FD14DAA 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000","u":"1 11523B940E760339BBDA36AE6B2DDA570E9CCC2E9314744FCB6C767DF022C5CF 1 1DADE6A6EBFFB2D329A691DB51C3A862F5FBD7D6BD5E594216E613BE882DBC02 1 0E4DE16A4C7514B7F1E09D1253F79B1D3127FD45AB2E535717BA2912F048D587 1 14A1436619A0C1B02302D66D78CE66027A1AAF44FC6FA0BA605E045526A76B76 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000","pk":"1 0CBF9F57DD1607305F2D6297E85CA9B1D71BCBDCE26E10329224C4EC3C0299D6 1 01EE49B4D07D933E518E9647105408758B1D7E977E66E976E4FE4A2E66F8E734 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8","y":"1 1D2618B8EA3B4E1C5C8D0382940E34DA19425E3CE69C2F6A55F10352ABDF7BD9 1 1F45619B4247A65FDFE577B6AE40474F53E94A83551622859795E71B44920FA0 1 21324A71042C04555C2C89881F297E6E4FB10BA3949B0C3C345B4E5EE4C48100 1 0FAF04961F119E50C72FF39E7E7198EBE46C2217A87A47C6A6A5BFAB6D39E1EE 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000"}} + })) + .unwrap(); + + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_credential_definition() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_credential_definition() + .return_const(true); + + let return_cred_def = cred_def.try_clone().unwrap(); + let expected_id = id.clone(); + reader2 + .expect_get_cred_def() + .times(1) + .withf(move |id, _| id == &expected_id) + .return_once(move |_, _| { + let cred_def = return_cred_def.try_clone().unwrap(); + Box::pin(async { Ok(cred_def) }) + }); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let actual_cred_def = reader.get_cred_def(&id, None).await.unwrap(); + assert_eq!( + serde_json::to_value(actual_cred_def).unwrap(), + serde_json::to_value(cred_def).unwrap() + ); + } + + #[tokio::test] + async fn test_get_rev_reg_def_proxy() { + let id = RevocationRegistryDefinitionId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let rev_reg_def: RevocationRegistryDefinition = serde_json::from_value(json!({ + "id": id, + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "credDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/8372c1bc-907d-44a9-86be-ac3672b26e2e", + "revocDefType": "CL_ACCUM", + "tag": "1.0", + "value": { + "maxCredNum": 5, + "publicKeys": { + "accumKey": { + "z": "1 10D3560CAE0591EEA7D7A63E1A362FC31448EF321E04FD75F248BBAF02DE9749 1 118C4B0C7F3D86D46C22D62BAE7E613B137A879B50EFDFC56451AB9012BA57A0 1 23D6952F9D058744D4930D1DE6D79548BDCA3EE8BAAF64B712668E52A1290547 1 14C4C4389D92A99C4DA7E6CC2BD0C82E2809D3CD202CD2F0AD6C33D75AA39049 1 174EACBC7981492A791A72D57C6CB9FE488A679C4A5674E4F3C247D73A827384 1 0172B8961122D4D825B282CA1CD1BBC3B8DC459994C9FE2827CDF74B3AB08D38 1 181159044E453DC59FF320E9E08C666176F6B9309E162E2DA4FC1DB3156F7B1F 1 2323CEBFB26C6D28CBAF5F87F155362C6FA14AFA0EBA7DE2B4154FE4082E30FD 1 2354CB1624B42A284B41E5B3B4489C2795DBA9B88A725005555FB698AFF97260 1 07EEEF48EF52E5B15FD4AC28F0DAEDE0A259A27500855992307518A0DBE29A83 1 00FE73BCDB27D1DAD37E4F0E424372CA9548F11B4EC977DCCCC53D99A5C66F36 1 07E9DC0DD2163A66EDA84CD6BF282C7E18CB821762B6047CA1AB9FBE94DC6546" + } + }, + "tailsHash": "GW1bmjcMmtHnLwbWrabX4sWYVopJMEvQWgYMAEDmbJS3", + "tailsLocation": "GW1bmjcMmtHnLwbWrabX4sWYVopJMEvQWgYMAEDmbJS3" + } + })) + .unwrap(); + let meta = json!({ + "foo": "bar", + uuid::Uuid::new_v4().to_string(): [uuid::Uuid::new_v4().to_string()], + }); + + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_revocation_registry() + .return_const(true); + + let return_def = rev_reg_def.clone(); + let return_meta = meta.clone(); + reader2 + .expect_get_rev_reg_def_json() + .times(1) + .with(eq(id.clone())) + .return_once(move |_| Ok((return_def, return_meta))); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let (actual_def, actual_meta) = reader.get_rev_reg_def_json(&id).await.unwrap(); + assert_eq!( + serde_json::to_value(actual_def).unwrap(), + serde_json::to_value(rev_reg_def).unwrap() + ); + assert_eq!( + serde_json::to_value(actual_meta).unwrap(), + serde_json::to_value(meta).unwrap() + ); + } + + #[tokio::test] + async fn test_get_rev_status_list_proxy() { + let id = RevocationRegistryDefinitionId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let input_timestamp = 978; + let meta = json!({ + "foo": "bar", + uuid::Uuid::new_v4().to_string(): [uuid::Uuid::new_v4().to_string()], + }); + let rev_status_list: RevocationStatusList = serde_json::from_value(json!({ + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "revRegDefId": "4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + "revocationList": [0, 1, 1, 0], + "currentAccumulator": "21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + "timestamp": 1669640864 + })) + .unwrap(); + let output_timestamp = 876; + + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_revocation_registry() + .return_const(true); + + let return_list = rev_status_list.clone(); + let expected_id = id.clone(); + let expected_meta = meta.clone(); + reader2 + .expect_get_rev_status_list() + .times(1) + .withf(move |id, ts, meta| { + id == &expected_id && ts == &input_timestamp && meta == &Some(&expected_meta) + }) + .return_once(move |_, _, _| { + Box::pin(async move { Ok((return_list, output_timestamp)) }) + }); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let (actual_list, actual_timestamp) = reader + .get_rev_status_list(&id, input_timestamp, Some(&meta)) + .await + .unwrap(); + assert_eq!( + serde_json::to_value(actual_list).unwrap(), + serde_json::to_value(rev_status_list).unwrap() + ); + assert_eq!( + serde_json::to_value(actual_timestamp).unwrap(), + serde_json::to_value(output_timestamp).unwrap() + ); + } + + #[allow(deprecated)] // TODO - https://github.com/hyperledger/aries-vcx/issues/1309 + #[tokio::test] + async fn test_get_rev_reg_delta_proxy() { + let id = RevocationRegistryDefinitionId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let rev_reg_delta: RevocationRegistryDelta = serde_json::from_value(json!({ + "value":{"accum":"2 0A0752AD393CCA8E840459E79BCF48F16ECEF17C00E9B639AC6CE2CCC93954C9 2 242D07E4AE3284C1E499D98E4EDF65ACFC0392E64C2BFF55192AC3AE51C3657C 2 165A2D44CAEE9717F1F52CC1BA6F72F39B21F969B3C4CDCA4FB501880F7AD297 2 1B08C9BB4876353F70E4A639F3B41593488B9964D4A56B61B0E1FF8B0FB0A1E7 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000"} + })) + .unwrap(); + let from = 123; + let to = 345; + let timestamp = 678; + + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_revocation_registry() + .return_const(true); + + let return_delta = rev_reg_delta.clone(); + reader2 + .expect_get_rev_reg_delta_json() + .times(1) + .with(eq(id.clone()), eq(Some(from)), eq(Some(to))) + .return_once(move |_, _, _| Ok((return_delta, timestamp))); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let (actual_delta, actual_timestamp) = reader + .get_rev_reg_delta_json(&id, Some(from), Some(to)) + .await + .unwrap(); + assert_eq!(actual_delta, rev_reg_delta); + assert_eq!(actual_timestamp, timestamp); + } + + #[tokio::test] + async fn test_get_rev_reg_proxy() { + let id = RevocationRegistryDefinitionId::new_unchecked(uuid::Uuid::new_v4().to_string()); + let rev_reg: RevocationRegistry = serde_json::from_value(json!({ + "value":{"accum":"2 0A0752AD393CCA8E840459E79BCF48F16ECEF17C00E9B639AC6CE2CCC93954C9 2 242D07E4AE3284C1E499D98E4EDF65ACFC0392E64C2BFF55192AC3AE51C3657C 2 165A2D44CAEE9717F1F52CC1BA6F72F39B21F969B3C4CDCA4FB501880F7AD297 2 1B08C9BB4876353F70E4A639F3B41593488B9964D4A56B61B0E1FF8B0FB0A1E7 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000"} + })) + .unwrap(); + let to = 345; + let timestamp = 678; + + let mut reader1 = MockReader::new(); + reader1 + .expect_supports_revocation_registry() + .return_const(false); + let mut reader2 = MockReader::new(); + reader2 + .expect_supports_revocation_registry() + .return_const(true); + + let return_reg = rev_reg.clone(); + reader2 + .expect_get_rev_reg() + .times(1) + .with(eq(id.clone()), eq(to)) + .return_once(move |_, _| Ok((return_reg, timestamp))); + + let reader = MultiLedgerAnoncredsRead::new() + .register_reader(reader1) + .register_reader(reader2); + + let (actual_reg, actual_timestamp) = reader.get_rev_reg(&id, to).await.unwrap(); + assert_eq!(actual_reg, rev_reg); + assert_eq!(actual_timestamp, timestamp); + } +} diff --git a/aries/aries_vcx_ledger/src/ledger/type_conversion.rs b/aries/aries_vcx_ledger/src/ledger/type_conversion.rs index e0f30ffa1d..8bd3046c86 100644 --- a/aries/aries_vcx_ledger/src/ledger/type_conversion.rs +++ b/aries/aries_vcx_ledger/src/ledger/type_conversion.rs @@ -232,8 +232,12 @@ impl Convert for IndyVdrRevocationRegistryDefinition { fn convert(self, (): Self::Args) -> Result { match self { IndyVdrRevocationRegistryDefinition::RevocationRegistryDefinitionV1(rev_reg_def) => { + let Some((issuer_id, _cred_def, _type, _tag)) = rev_reg_def.id.parts() else { + return Err(format!("rev reg id is not valid: {}", rev_reg_def.id).into()); + }; Ok(OurRevocationRegistryDefinition { id: OurRevocationRegistryDefinitionId::new(rev_reg_def.id.to_string())?, + issuer_id: IssuerId::new(issuer_id.to_string())?, revoc_def_type: anoncreds_types::data_types::ledger::rev_reg_def::RegistryType::CL_ACCUM, tag: rev_reg_def.tag, diff --git a/aries/aries_vcx_ledger/tests/test_cheqd.rs b/aries/aries_vcx_ledger/tests/test_cheqd.rs new file mode 100644 index 0000000000..65fa3561b2 --- /dev/null +++ b/aries/aries_vcx_ledger/tests/test_cheqd.rs @@ -0,0 +1,227 @@ +#[cfg(feature = "cheqd")] +mod test_cheqd { + use std::sync::Arc; + + use anoncreds_types::data_types::identifiers::{ + cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, + schema_id::SchemaId, + }; + use aries_vcx_ledger::ledger::{ + base_ledger::AnoncredsLedgerRead, cheqd::CheqdAnoncredsLedgerRead, + }; + use chrono::{DateTime, Utc}; + use did_cheqd::resolution::resolver::DidCheqdResolver; + use serde_json::json; + + #[tokio::test] + async fn test_resolve_schema_vector() { + let id = "did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675/resources/\ + a7e2fc0b-5f6c-466d-911f-3ed9909f98a0"; + + let reader = + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))); + let schema = reader + .get_schema(&SchemaId::new_unchecked(id), None) + .await + .unwrap(); + + assert_eq!( + schema.id.0, + "did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675/resources/\ + a7e2fc0b-5f6c-466d-911f-3ed9909f98a0" + ); + assert!(schema.seq_no.is_none()); + assert_eq!( + schema.name, + "Faber College221a463c-9160-41bd-839c-26c0154e64b4" + ); + assert_eq!(schema.version, "1.0.0"); + assert_eq!( + schema.attr_names.0, + vec!["name".to_string(), "degree".to_string(), "date".to_string()] + ); + assert_eq!( + schema.issuer_id.0, + "did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675" + ); + } + + #[tokio::test] + async fn test_resolve_cred_def_vector() { + let id = "did:cheqd:testnet:e5d13e49-9f5d-4ec1-b0f6-43e43e211fdc/resources/\ + 796f4d32-ceb2-4549-ac2f-5270442066ee"; + + let reader = + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))); + let cred_def = reader + .get_cred_def(&CredentialDefinitionId::new_unchecked(id), None) + .await + .unwrap(); + + let expected_cred_def = json!({ + "id": "did:cheqd:testnet:e5d13e49-9f5d-4ec1-b0f6-43e43e211fdc/resources/796f4d32-ceb2-4549-ac2f-5270442066ee", + "schemaId": "did:cheqd:testnet:e5d13e49-9f5d-4ec1-b0f6-43e43e211fdc/resources/441dd8ac-5132-4f64-a899-b95e6631861e", + "issuerId": "did:cheqd:testnet:e5d13e49-9f5d-4ec1-b0f6-43e43e211fdc", + "type": "CL", + "tag": "default", + "value": { + "primary": { + "n": "101775425686705322446321729042513323885366249575341827532852151406029439141945446540758073370857140650173543806620665009319990704696336867406228341102050525062347572470125847326218665243292282264450928863478570405137509031729993280909749863406879170040618211281523178059425360274769809374009133917080596452472404002929533556061638640203000980751755775872098282217341230637597866343991247907713086791452810236342936381540330526648988208357641862302546515645514361066795780511377402835905056549804798920667891392124801399775162573084938362756949887199195057995844386465801420665147294058855188546320799079663835174965161", + "s": "87624888798698822624423723118022809956578348937987561814948126500576219529138836392203224198856904274580002516694066594875873709134352028429835540535481267491635062762312964551217817275494607969045880749427396603031296648191143611156088073589185938269488488710758969627092198856856923016111781105026554515570693238770759473619839075955094865571664244739744514364819888950198503617844415579864953624998989292124086306326904837728507294638098267220789662137529137088453854836926570472177996704875536555330577801472880881494686752967061366433608898978631273502532992063649958617359359105975313298611541375812686478449038", + "r": { + "master_secret": "91924390643616684447850600547636799126754942750676961814085841416767517190041327602185387580658201961018441581408151006589910605450989653472590754296936606411587277500591300185962955561382125964973024308169242022915814560288197996510864084499323589139928149142636050230482921777834206450457769957179088070681863843269743734589083210580654397696548923614457471055030224454430850991434040684872112181784382757277123431379517419634053875223449800478697799219665093330299855414452721993787344846427989123844376930340324838760730841881922239261134345165306574342224223006710126470962389247658692615899692622733364593917564", + "dataload": "67714311012680607861506009159005649926100729749085079545683454286626632138688065577440485138428200490485338821059098371694307470028480026620243200576189622077496672555428629091259952610415973355058680447309063025743992477107070451623444621247413013233746035427316025697312475570466580668335703497887313077562889740624862997672438829468032595482449521331150586223865869041877654993640507137080181293240650234816519778512756819260970205819993241324592879273813227162717013131055606974830594578099412351827306325727807837670155477487273346541222802392212114521431844422126972523175992819888243089660896279345668836709183" + }, + "rctxt": "70939857802453506951149531957606930306640909143475371737027498474152925628494791068427574134203017421399121411176717498176846791145767680818780201808144435771494206471213311901071561885391866584823165735626586292923926605780832222900819531483444405585980754893162270863536237119860353096313485759974542267053904367917010014776300492094349532540865655521444795825149399229035168301897753439893554059797022750502266578483363311220307405821402958792359030164217593034199227560018630509640528678991350608730838863727066178052927862093157207477972326979317508513953451471067387162273207269626177777770178388199904693271885", + "z": "67232321822071084762251502223976923452971987672236221455852097322998038231254751227728590284858878856391984973291870462921522030038401971062122863827666305436738444365691249161806642192223615405177957760215302017704093487843885193856291620515859197624091514138527124658905269978674424356277491558952327833769860308310713639320922734643110516571614031976998124656051686500162012298658320291610287606636134513132238361082981123202624198501889516057149568201642936231925672511435865393828765935813568402464860650327397205857299165873490962876370815478186692229961439123671741775783729284710421491763990499547934996243081" + } + } + }); + assert_eq!(serde_json::to_value(cred_def).unwrap(), expected_cred_def); + } + + // https://testnet-explorer.cheqd.io/transactions/92C31ED20512FEE73EA4D8A6C8E63E652AA61A14D4F8C00203312EA185419CB9 + #[tokio::test] + async fn test_resolve_rev_reg_def_vector() { + let id = "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/\ + 4f265d83-4657-4c37-ba80-c66cc399457e"; + + let reader = + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))); + let (rev_reg_def, meta) = reader + .get_rev_reg_def_json(&RevocationRegistryDefinitionId::new_unchecked(id)) + .await + .unwrap(); + + let expected_rev_reg_def = json!({ + "id": id, + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "credDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/8372c1bc-907d-44a9-86be-ac3672b26e2e", + "revocDefType": "CL_ACCUM", + "tag": "1.0", + "value": { + "maxCredNum": 5, + "publicKeys": { + "accumKey": { + "z": "1 10D3560CAE0591EEA7D7A63E1A362FC31448EF321E04FD75F248BBAF02DE9749 1 118C4B0C7F3D86D46C22D62BAE7E613B137A879B50EFDFC56451AB9012BA57A0 1 23D6952F9D058744D4930D1DE6D79548BDCA3EE8BAAF64B712668E52A1290547 1 14C4C4389D92A99C4DA7E6CC2BD0C82E2809D3CD202CD2F0AD6C33D75AA39049 1 174EACBC7981492A791A72D57C6CB9FE488A679C4A5674E4F3C247D73A827384 1 0172B8961122D4D825B282CA1CD1BBC3B8DC459994C9FE2827CDF74B3AB08D38 1 181159044E453DC59FF320E9E08C666176F6B9309E162E2DA4FC1DB3156F7B1F 1 2323CEBFB26C6D28CBAF5F87F155362C6FA14AFA0EBA7DE2B4154FE4082E30FD 1 2354CB1624B42A284B41E5B3B4489C2795DBA9B88A725005555FB698AFF97260 1 07EEEF48EF52E5B15FD4AC28F0DAEDE0A259A27500855992307518A0DBE29A83 1 00FE73BCDB27D1DAD37E4F0E424372CA9548F11B4EC977DCCCC53D99A5C66F36 1 07E9DC0DD2163A66EDA84CD6BF282C7E18CB821762B6047CA1AB9FBE94DC6546" + } + }, + "tailsHash": "GW1bmjcMmtHnLwbWrabX4sWYVopJMEvQWgYMAEDmbJS3", + "tailsLocation": "GW1bmjcMmtHnLwbWrabX4sWYVopJMEvQWgYMAEDmbJS3" + } + }); + assert_eq!( + serde_json::to_value(rev_reg_def).unwrap(), + expected_rev_reg_def + ); + + assert_eq!( + meta.resource_name, + "275990cc056b46176a7122cfd888f46a2bd8e3d45a71d5ff20764a874ed02edd" + ); + } + + // test status list resolution from credo-ts uploaded vectors + // https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b?resourceName=275990cc056b46176a7122cfd888f46a2bd8e3d45a71d5ff20764a874ed02edd&resourceType=anonCredsStatusList&resourceMetadata=true + // reset: https://testnet-explorer.cheqd.io/transactions/356F65125E585B9F439C423F2AD7DE73ADF4DC9A0811AA8EE5D03D63B1872DC0 + // 2024-12-04T22:14:55Z + // update 1: https://testnet-explorer.cheqd.io/transactions/ADF7D562A5005576FA6EF8DC864DAA306EB62C40911FEB5B30C8F98968AE7B51 + // 2024-12-04T22:15:07Z + // update 2: https://testnet-explorer.cheqd.io/transactions/222FF2D023C2C9A097BB38F3875F072DF8DEC7B0CBD46AC3459C9B4C3C74382F + // 2024-12-04T22:15:18Z + // update 3: https://testnet-explorer.cheqd.io/transactions/791D57B8C49C270B3EDA0E9E7E00811CA828190C2D6517FDE8E40CD8FE445E1C + // 2024-12-04T22:15:30Z + #[tokio::test] + async fn test_resolve_rev_status_list_versions() { + let def_id = "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/\ + 4f265d83-4657-4c37-ba80-c66cc399457e"; + let def_id = RevocationRegistryDefinitionId::new_unchecked(def_id); + + let init_time = DateTime::parse_from_rfc3339("2024-12-04T22:14:55Z") + .unwrap() + .timestamp() as u64; + let update1_time = DateTime::parse_from_rfc3339("2024-12-04T22:15:07Z") + .unwrap() + .timestamp() as u64; + let update2_time = DateTime::parse_from_rfc3339("2024-12-04T22:15:18Z") + .unwrap() + .timestamp() as u64; + let update3_time = DateTime::parse_from_rfc3339("2024-12-04T22:15:30Z") + .unwrap() + .timestamp() as u64; + + let reader = + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))); + + let def_meta = reader.get_rev_reg_def_json(&def_id).await.unwrap().1; + + // scenario 1: get most recent + let now = Utc::now().timestamp() as u64; + let (status_list, update_time) = reader + .get_rev_status_list(&def_id, now, Some(&def_meta)) + .await + .unwrap(); + assert_eq!(update_time, update3_time); + assert_eq!( + serde_json::to_value(status_list).unwrap(), + json!({ + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "revRegDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/4f265d83-4657-4c37-ba80-c66cc399457e", + "revocationList": [1,1,1,1,0], + "currentAccumulator": "21 114BE4F2BBAAF18F07E994D74B28347FA0BEC500A616B47F57F2E0B0864F7602E 21 12AB68E307C5F2AA30F34A03ADB298C7F4C02555649E510919979C2AEB49CCDF1 6 5FB9FB957339A842130C84FC98240A163E56DC58B96423F1EFD53E9106671B94 4 28F2F8297E345FFF55CDEE87C83DE471486826C91EBBA2C39570A46013B5BFBA 6 565A830A4358E1F6F21A10804C23E36D739B5630C6A188D760F4B6F434D1311D 4 14F87165B42A780974AC70669DC3CF629F1103DF73AE15AC11A1151883A91941", + "timestamp": update3_time + }) + ); + + // scenario 2: between update 2 & 3 + let (status_list, update_time) = reader + .get_rev_status_list(&def_id, update2_time + 3, Some(&def_meta)) + .await + .unwrap(); + assert_eq!(update_time, update2_time); + assert_eq!( + serde_json::to_value(status_list).unwrap(), + json!({ + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "currentAccumulator": "21 125DF938B3B772619CB43E561D69004CF09667376E9CD53C818D84860BAE3D1D9 21 11ECFC5F9B469AC74E2A0E329F86C6E60B423A53CAC5AE7A4DBE7A978BFFC0DA1 6 6FAD628FED470FF640BF2C5DB57C2C18D009645DBEF15D4AF710739D2AD93E2D 4 22093A3300411B059B8BB7A8C3296A2ED9C4C8E00106C3B2BAD76E25AC792063 6 71D70ECA81BCE610D1C22CADE688AF4A122C8258E8B306635A111D0A35A7238A 4 1E80F38ABA3A966B8657D722D4E956F076BB2F5CCF36AA8942E65500F8898FF3", + "revRegDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/4f265d83-4657-4c37-ba80-c66cc399457e", + "revocationList": [1,1,1,0,0], + "timestamp": update2_time + }) + ); + + // scenario 3: between update 1 & 2 + let (status_list, update_time) = reader + .get_rev_status_list(&def_id, update1_time + 3, Some(&def_meta)) + .await + .unwrap(); + assert_eq!(update_time, update1_time); + assert_eq!( + serde_json::to_value(status_list).unwrap(), + json!({ + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "currentAccumulator": "21 136FA865B5CD0AEA1DA05BE412C6E06C23066C338D39C5B79C5E1AE1D5BA20AAA 21 124182E098BE418B9DBECF600EEA3D070EDB85D6B412EE75B4B43C440FEA2E631 6 669D66FB3BC245B4EF892B8DB5A330ACA6A4CE6706FB58D9B487C0487DBB5C04 4 2C5C9551DFE2A4AE71D355DD3A981F155F51B9BCF8E2ED8B8263726DDF60D09C 6 7243CF31A80313C254F51D2B0A3573320B885178F36F4AE1E8FF4A520EF9CDCA 4 1B8DBE9563FAD9FBF8B75BCE41C9425E1D15EE0B3D195D0A86AD8A2C91D5BB73", + "revRegDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/4f265d83-4657-4c37-ba80-c66cc399457e", + "revocationList": [1,1,0,0,0], + "timestamp": update1_time + }) + ); + + // scenario 4: between init & update 1 + let (status_list, update_time) = reader + .get_rev_status_list(&def_id, init_time + 3, Some(&def_meta)) + .await + .unwrap(); + assert_eq!(update_time, init_time); + assert_eq!( + serde_json::to_value(status_list).unwrap(), + json!({ + "issuerId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "currentAccumulator": "1 0443A0BC791EE82B8F34066404B36E81E0CE68B64BD2A48A55587E4585B16CCA 1 0343A5D644B28DCC0EAF9C6D3E104DC0F61FCD711AFE93DB67031905DAA5F654 1 02CE577295DF112BB2C7F16250D4593FC922B074436EC0F4F124E2409EF99785 1 1692EE5DFE9885809DA503A2EEDC4EECDA5D7D415C743E3931576EFD72FB51AC 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000", + "revRegDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/4f265d83-4657-4c37-ba80-c66cc399457e", + "revocationList": [0,0,0,0,0], + "timestamp": init_time + }) + ); + } +} diff --git a/aries/aries_vcx_ledger/tests/test_multi_ledger.rs b/aries/aries_vcx_ledger/tests/test_multi_ledger.rs new file mode 100644 index 0000000000..85c86decf4 --- /dev/null +++ b/aries/aries_vcx_ledger/tests/test_multi_ledger.rs @@ -0,0 +1,48 @@ +#[cfg(feature = "cheqd")] +mod test_cheqd { + use std::sync::Arc; + + use aries_vcx_ledger::{ + errors::error::VcxLedgerResult, + ledger::{ + cheqd::CheqdAnoncredsLedgerRead, + indy_vdr_ledger::{IndyVdrLedgerRead, IndyVdrLedgerReadConfig}, + multi_ledger::MultiLedgerAnoncredsRead, + request_submitter::RequestSubmitter, + response_cacher::noop::NoopResponseCacher, + }, + }; + use async_trait::async_trait; + use did_cheqd::resolution::resolver::DidCheqdResolver; + use indy_vdr::pool::ProtocolVersion; + use mockall::mock; + + mock! { + pub RequestSubmitter {} + #[async_trait] + impl RequestSubmitter for RequestSubmitter { + async fn submit(&self, request: indy_vdr::pool::PreparedRequest) -> VcxLedgerResult; + } + } + + fn dummy_indy_vdr_reader() -> IndyVdrLedgerRead { + IndyVdrLedgerRead::new(IndyVdrLedgerReadConfig { + request_submitter: MockRequestSubmitter::new(), + response_parser: indy_ledger_response_parser::ResponseParser, + response_cacher: NoopResponseCacher, + protocol_version: ProtocolVersion::Node1_4, + }) + } + + // asserts the successful construction using our defined anoncreds ledger readers. + #[test] + fn test_construction() { + let cheqd = + CheqdAnoncredsLedgerRead::new(Arc::new(DidCheqdResolver::new(Default::default()))); + let indy = dummy_indy_vdr_reader(); + + let _multi_ledger = MultiLedgerAnoncredsRead::new() + .register_reader(cheqd) + .register_reader(indy); + } +} diff --git a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/issue_credential.rs b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/issue_credential.rs index 6a92d04031..a4b7323a7b 100644 --- a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/issue_credential.rs +++ b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/issue_credential.rs @@ -43,6 +43,8 @@ pub struct IssueCredentialV2Decorators { pub enum IssueCredentialAttachmentFormatType { #[serde(rename = "aries/ld-proof-vc@v1.0")] AriesLdProofVc1_0, + #[serde(rename = "anoncreds/credential@v1.0")] + AnoncredsCredential1_0, #[serde(rename = "hlindy/cred@v2.0")] HyperledgerIndyCredential2_0, } diff --git a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/offer_credential.rs b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/offer_credential.rs index 39a499a3e3..4bf242e348 100644 --- a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/offer_credential.rs +++ b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/offer_credential.rs @@ -45,6 +45,8 @@ pub enum OfferCredentialAttachmentFormatType { DifCredentialManifest1_0, #[serde(rename = "hlindy/cred-abstract@v2.0")] HyperledgerIndyCredentialAbstract2_0, + #[serde(rename = "anoncreds/credential-offer@v1.0")] + AnoncredsCredentialOffer1_0, #[serde(rename = "aries/ld-proof-vc-detail@v1.0")] AriesLdProofVcDetail1_0, } diff --git a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/propose_credential.rs b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/propose_credential.rs index f197c90783..2c60094f25 100644 --- a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/propose_credential.rs +++ b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/propose_credential.rs @@ -44,6 +44,8 @@ pub enum ProposeCredentialAttachmentFormatType { DifCredentialManifest1_0, #[serde(rename = "aries/ld-proof-vc-detail@v1.0")] AriesLdProofVcDetail1_0, + #[serde(rename = "anoncreds/credential-filter@v1.0")] + AnoncredCredentialFilter1_0, #[serde(rename = "hlindy/cred-filter@v2.0")] HyperledgerIndyCredentialFilter2_0, } diff --git a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/request_credential.rs b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/request_credential.rs index 210c5ad0e3..436ee9db70 100644 --- a/aries/messages/src/msg_fields/protocols/cred_issuance/v2/request_credential.rs +++ b/aries/messages/src/msg_fields/protocols/cred_issuance/v2/request_credential.rs @@ -40,6 +40,8 @@ pub enum RequestCredentialAttachmentFormatType { DifCredentialManifest1_0, #[serde(rename = "hlindy/cred-req@v2.0")] HyperledgerIndyCredentialRequest2_0, + #[serde(rename = "anoncreds/credential-request@v1.0")] + AnoncredsCredentialRequest1_0, #[serde(rename = "aries/ld-proof-vc-detail@v1.0")] AriesLdProofVcDetail1_0, } diff --git a/aries/messages/src/msg_fields/protocols/present_proof/v2/present.rs b/aries/messages/src/msg_fields/protocols/present_proof/v2/present.rs index 1409dc48ec..226728abf1 100644 --- a/aries/messages/src/msg_fields/protocols/present_proof/v2/present.rs +++ b/aries/messages/src/msg_fields/protocols/present_proof/v2/present.rs @@ -42,6 +42,8 @@ pub struct PresentationV2Decorators { pub enum PresentationAttachmentFormatType { #[serde(rename = "hlindy/proof@v2.0")] HyperledgerIndyProof2_0, + #[serde(rename = "anoncreds/proof@v1.0")] + AnoncredsProof1_0, #[serde(rename = "dif/presentation-exchange/submission@v1.0")] DifPresentationExchangeSubmission1_0, } diff --git a/aries/messages/src/msg_fields/protocols/present_proof/v2/propose.rs b/aries/messages/src/msg_fields/protocols/present_proof/v2/propose.rs index 97386b4817..38652489f5 100644 --- a/aries/messages/src/msg_fields/protocols/present_proof/v2/propose.rs +++ b/aries/messages/src/msg_fields/protocols/present_proof/v2/propose.rs @@ -43,6 +43,8 @@ pub enum ProposePresentationAttachmentFormatType { DifPresentationExchangeDefinitions1_0, #[serde(rename = "hlindy/proof-req@v2.0")] HyperledgerIndyProofRequest2_0, + #[serde(rename = "anoncreds/proof-request@v1.0")] + AnoncredsProofRequest1_0, } #[cfg(test)] diff --git a/aries/messages/src/msg_fields/protocols/present_proof/v2/request.rs b/aries/messages/src/msg_fields/protocols/present_proof/v2/request.rs index d07fb10699..af86c18d5c 100644 --- a/aries/messages/src/msg_fields/protocols/present_proof/v2/request.rs +++ b/aries/messages/src/msg_fields/protocols/present_proof/v2/request.rs @@ -44,6 +44,8 @@ pub struct RequestPresentationV2Decorators { pub enum PresentationRequestAttachmentFormatType { #[serde(rename = "hlindy/proof-req@v2.0")] HyperledgerIndyProofRequest2_0, + #[serde(rename = "anoncreds/proof-request@v1.0")] + AnoncredsProofRequest1_0, #[serde(rename = "dif/presentation-exchange/definitions@v1.0")] DifPresentationExchangeDefinitions1_0, } diff --git a/aries/misc/anoncreds_types/src/data_types/ledger/rev_reg_def.rs b/aries/misc/anoncreds_types/src/data_types/ledger/rev_reg_def.rs index 61a25eb3fd..44633e6803 100644 --- a/aries/misc/anoncreds_types/src/data_types/ledger/rev_reg_def.rs +++ b/aries/misc/anoncreds_types/src/data_types/ledger/rev_reg_def.rs @@ -5,7 +5,8 @@ use anoncreds_clsignatures::RevocationKeyPrivate; use crate::{ cl::RevocationKeyPublic, data_types::identifiers::{ - cred_def_id::CredentialDefinitionId, rev_reg_def_id::RevocationRegistryDefinitionId, + cred_def_id::CredentialDefinitionId, issuer_id::IssuerId, + rev_reg_def_id::RevocationRegistryDefinitionId, }, utils::validation::Validatable, }; @@ -51,7 +52,7 @@ pub struct RevocationRegistryDefinitionValuePublicKeys { #[serde(rename_all = "camelCase")] pub struct RevocationRegistryDefinition { pub id: RevocationRegistryDefinitionId, - // pub issuer_id: IssuerId, // This is not on ledger + pub issuer_id: IssuerId, pub revoc_def_type: RegistryType, pub tag: String, pub cred_def_id: CredentialDefinitionId, @@ -61,7 +62,7 @@ pub struct RevocationRegistryDefinition { impl Validatable for RevocationRegistryDefinition { fn validate(&self) -> Result<(), crate::error::Error> { self.cred_def_id.validate()?; - // self.issuer_id.validate()?; + self.issuer_id.validate()?; Ok(()) } diff --git a/aries/misc/anoncreds_types/src/data_types/ledger/rev_status_list.rs b/aries/misc/anoncreds_types/src/data_types/ledger/rev_status_list.rs index 6d735fa27a..8ce242e44c 100644 --- a/aries/misc/anoncreds_types/src/data_types/ledger/rev_status_list.rs +++ b/aries/misc/anoncreds_types/src/data_types/ledger/rev_status_list.rs @@ -13,18 +13,18 @@ use crate::{ #[serde(rename_all = "camelCase")] pub struct RevocationStatusList { #[serde(skip_serializing_if = "Option::is_none")] - rev_reg_def_id: Option, - issuer_id: IssuerId, + pub rev_reg_def_id: Option, + pub issuer_id: IssuerId, #[serde(with = "serde_revocation_list")] - revocation_list: bitvec::vec::BitVec, + pub revocation_list: bitvec::vec::BitVec, #[serde( rename = "currentAccumulator", alias = "accum", skip_serializing_if = "Option::is_none" )] - accum: Option, + pub accum: Option, #[serde(skip_serializing_if = "Option::is_none")] - timestamp: Option, + pub timestamp: Option, } impl From<&RevocationStatusList> for Option { diff --git a/aries/misc/anoncreds_types/src/utils/conversions.rs b/aries/misc/anoncreds_types/src/utils/conversions.rs new file mode 100644 index 0000000000..1230753994 --- /dev/null +++ b/aries/misc/anoncreds_types/src/utils/conversions.rs @@ -0,0 +1,65 @@ +use bitvec::bitvec; + +use crate::data_types::{ + identifiers::{issuer_id::IssuerId, rev_reg_def_id::RevocationRegistryDefinitionId}, + ledger::{rev_reg_delta::RevocationRegistryDeltaValue, rev_status_list::RevocationStatusList}, +}; + +/// Converts from a [RevocationRegistryDeltaValue] into a completed [RevocationStatusList] +/// (newer format). +/// +/// NOTE: this conversion only works if the delta was calculated from START (timestamp 0/None) +/// to `timestamp`. +pub fn from_revocation_registry_delta_to_revocation_status_list( + delta: &RevocationRegistryDeltaValue, + timestamp: u64, + rev_reg_id: &RevocationRegistryDefinitionId, + max_cred_num: usize, + issuer_id: IssuerId, +) -> Result { + // no way to derive this value here. So we assume true, as false (ISSAUNCE_ON_DEAMAND) is not + // recomended by anoncreds: https://hyperledger.github.io/anoncreds-spec/#anoncreds-issuer-setup-with-revocation + let issuance_by_default = true; + let default_state = if issuance_by_default { 0 } else { 1 }; + let mut revocation_list = bitvec![default_state; max_cred_num]; + let revocation_len = revocation_list.len(); + + for issued in &delta.issued { + if revocation_len <= *issued as usize { + return Err(crate::Error::from_msg( + crate::ErrorKind::ConversionError, + format!( + "Error whilst constructing a revocation status list from the ledger's delta. \ + Ledger delta reported an issuance for cred_rev_id '{issued}', but the \ + revocation_list max size is {revocation_len}" + ), + )); + } + revocation_list.insert(*issued as usize, false); + } + + for revoked in &delta.revoked { + if revocation_len <= *revoked as usize { + return Err(crate::Error::from_msg( + crate::ErrorKind::ConversionError, + format!( + "Error whilst constructing a revocation status list from the ledger's delta. \ + Ledger delta reported an revocation for cred_rev_id '{revoked}', but the \ + revocation_list max size is {revocation_len}" + ), + )); + } + revocation_list.insert(*revoked as usize, true); + } + + let accum = delta.accum.into(); + + RevocationStatusList::new( + Some(&rev_reg_id.to_string()), + issuer_id, + revocation_list, + Some(accum), + Some(timestamp), + ) + .map_err(Into::into) +} diff --git a/aries/misc/anoncreds_types/src/utils/mod.rs b/aries/misc/anoncreds_types/src/utils/mod.rs index c5dc163117..b2e7a35f49 100644 --- a/aries/misc/anoncreds_types/src/utils/mod.rs +++ b/aries/misc/anoncreds_types/src/utils/mod.rs @@ -1,2 +1,4 @@ +/// VCX utility additon +pub mod conversions; pub mod query; pub mod validation; diff --git a/aries/misc/test_utils/src/constants.rs b/aries/misc/test_utils/src/constants.rs index 7e7e780ee1..ea5070f056 100644 --- a/aries/misc/test_utils/src/constants.rs +++ b/aries/misc/test_utils/src/constants.rs @@ -57,6 +57,7 @@ pub static LICENCE_CRED_ID: &str = "92556f60-d290-4b58-9a43-05c25aac214e"; pub static REV_REG_ID: &str = r#"V4SGRU86Z58d6TV7PBUe6f:4:V4SGRU86Z58d6TV7PBUe6f:3:CL:1281:tag1:CL_ACCUM:tag1"#; pub static REV_REG_DELTA_JSON: &str = r#"{"ver":"1.0","value":{"accum":"2 0A0752AD393CCA8E840459E79BCF48F16ECEF17C00E9B639AC6CE2CCC93954C9 2 242D07E4AE3284C1E499D98E4EDF65ACFC0392E64C2BFF55192AC3AE51C3657C 2 165A2D44CAEE9717F1F52CC1BA6F72F39B21F969B3C4CDCA4FB501880F7AD297 2 1B08C9BB4876353F70E4A639F3B41593488B9964D4A56B61B0E1FF8B0FB0A1E7 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000"}}"#; +pub static REV_STATUS_LIST_JSON: &str = r#"{"revRegDefId": "4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960","issuerId": "did:web:example.org","revocationList": [0, 1, 1, 0],"currentAccumulator": "21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C","timestamp": 1669640864}"#; pub static REV_STATE_JSON: &str = r#"{"rev_reg":{"accum":"1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000"},"timestamp":100,"witness":{"omega":"1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000"}}"#; pub static REV_REG_JSON: &str = r#"{"value":{"accum":"2 0204F2D2B1F2B705A11AAFEEE73C9BA084C12AF1179294529AC4D14CA54E87F3 2 222BAE38FAF2673F7BCBB86D8DE1A327F5065BDC892E9A122164260C97BC0C63 2 1565105F8BA53037978B66E0CC9F53205F189DEEB6B7168744456DD98D2F4E88 2 1AC9E76B2868141A42329778831C14AEAAF7A9981209C1D96AECA4E69CAFB243 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000"}}"#; pub static TEST_TAILS_LOCATION: &str = r#"/var/folders/kr/9gkxsj_s01b6fvx_72trl3mm0000gp/T/tails_file/5R6BWXL3vPrbJPKe9FsHAVG9hqKdDvVxonBuj3ETYuZh"#; @@ -66,6 +67,7 @@ pub static REQUEST_WITH_ENDORSER: &str = r#"{"seqNo":344,"reqId":152286672972686 pub fn rev_def_json() -> RevocationRegistryDefinition { serde_json::from_value(json!({ + "issuerId": INSTITUTION_DID, "ver":"1.0", "id": REV_REG_ID.to_string(), "revocDefType":"CL_ACCUM", diff --git a/aries/misc/test_utils/src/mockdata/mock_anoncreds.rs b/aries/misc/test_utils/src/mockdata/mock_anoncreds.rs index 6f2cf5cdac..e09d19f949 100644 --- a/aries/misc/test_utils/src/mockdata/mock_anoncreds.rs +++ b/aries/misc/test_utils/src/mockdata/mock_anoncreds.rs @@ -10,6 +10,7 @@ use anoncreds_types::data_types::{ rev_reg::RevocationRegistry, rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, + rev_status_list::RevocationStatusList, schema::{AttributeNames, Schema}, }, messages::{ @@ -178,8 +179,7 @@ impl BaseAnonCreds for MockAnoncreds { &self, _tails_dir: &Path, _rev_reg_def_json: RevocationRegistryDefinition, - _rev_reg_delta_json: RevocationRegistryDelta, - _timestamp: u64, + _rev_status_list: RevocationStatusList, _cred_rev_id: u32, ) -> VcxAnoncredsResult { Ok(serde_json::from_str(REV_STATE_JSON)?) @@ -188,10 +188,11 @@ impl BaseAnonCreds for MockAnoncreds { async fn prover_store_credential( &self, _wallet: &impl BaseWallet, - _cred_req_metadata_json: CredentialRequestMetadata, - _cred_json: Credential, - _cred_def_json: CredentialDefinition, - _rev_reg_def_json: Option, + _cred_req_metadata: CredentialRequestMetadata, + _cred: Credential, + _schema: Schema, + _cred_def: CredentialDefinition, + _rev_reg_def: Option, ) -> VcxAnoncredsResult { Ok("cred_id".to_string()) } diff --git a/aries/misc/test_utils/src/mockdata/mock_ledger.rs b/aries/misc/test_utils/src/mockdata/mock_ledger.rs index 0297255088..ee90a29610 100644 --- a/aries/misc/test_utils/src/mockdata/mock_ledger.rs +++ b/aries/misc/test_utils/src/mockdata/mock_ledger.rs @@ -6,7 +6,7 @@ use anoncreds_types::data_types::{ ledger::{ cred_def::CredentialDefinition, rev_reg::RevocationRegistry, rev_reg_def::RevocationRegistryDefinition, rev_reg_delta::RevocationRegistryDelta, - schema::Schema, + rev_status_list::RevocationStatusList, schema::Schema, }, }; use aries_vcx_ledger::{ @@ -23,7 +23,7 @@ use public_key::Key; use crate::constants::{ rev_def_json, CRED_DEF_JSON, DEFAULT_AUTHOR_AGREEMENT, REQUEST_WITH_ENDORSER, - REV_REG_DELTA_JSON, REV_REG_JSON, SCHEMA_JSON, + REV_REG_DELTA_JSON, REV_REG_JSON, REV_STATUS_LIST_JSON, SCHEMA_JSON, }; #[derive(Debug)] @@ -115,6 +115,8 @@ impl IndyLedgerWrite for MockLedger { #[allow(unused)] #[async_trait] impl AnoncredsLedgerRead for MockLedger { + type RevocationRegistryDefinitionAdditionalMetadata = (); + async fn get_schema( &self, schema_id: &SchemaId, @@ -134,8 +136,8 @@ impl AnoncredsLedgerRead for MockLedger { async fn get_rev_reg_def_json( &self, rev_reg_id: &RevocationRegistryDefinitionId, - ) -> VcxLedgerResult { - Ok(rev_def_json()) + ) -> VcxLedgerResult<(RevocationRegistryDefinition, ())> { + Ok((rev_def_json(), ())) } async fn get_rev_reg_delta_json( @@ -147,6 +149,15 @@ impl AnoncredsLedgerRead for MockLedger { Ok((serde_json::from_str(REV_REG_DELTA_JSON).unwrap(), 1)) } + async fn get_rev_status_list( + &self, + rev_reg_id: &RevocationRegistryDefinitionId, + timestamp: u64, + meta: Option<&()>, + ) -> VcxLedgerResult<(RevocationStatusList, u64)> { + Ok((serde_json::from_str(REV_STATUS_LIST_JSON).unwrap(), 1)) + } + async fn get_rev_reg( &self, rev_reg_id: &RevocationRegistryDefinitionId, diff --git a/did_core/did_methods/did_cheqd/Cargo.toml b/did_core/did_methods/did_cheqd/Cargo.toml index 371e6724f2..22bf389252 100644 --- a/did_core/did_methods/did_cheqd/Cargo.toml +++ b/did_core/did_methods/did_cheqd/Cargo.toml @@ -28,7 +28,7 @@ serde_json = "1.0.96" serde = { version = "1.0.160", features = ["derive"] } thiserror = "1.0.40" tokio = { version = "1.38.0" } -chrono = { version = "0.4.24", default-features = false } +chrono = { version = "0.4.24", default-features = false, features = ["now"] } url = { version = "2.3.1", default-features = false } bytes = "1.8.0" diff --git a/did_core/did_methods/did_cheqd/src/error/mod.rs b/did_core/did_methods/did_cheqd/src/error/mod.rs index fe3d49efdc..a60cbf775b 100644 --- a/did_core/did_methods/did_cheqd/src/error/mod.rs +++ b/did_core/did_methods/did_cheqd/src/error/mod.rs @@ -22,6 +22,10 @@ pub enum DidCheqdError { InvalidResponse(String), #[error("Invalid DID Document structure resolved: {0}")] InvalidDidDocument(String), + #[error("Invalid DID Url: {0}")] + InvalidDidUrl(String), + #[error("Resource could not be found: {0}")] + ResourceNotFound(String), #[error("Parsing error: {0}")] ParsingError(#[from] ParsingErrorSource), #[error(transparent)] diff --git a/did_core/did_methods/did_cheqd/src/resolution/resolver.rs b/did_core/did_methods/did_cheqd/src/resolution/resolver.rs index 4b063e35db..98cb8b5255 100644 --- a/did_core/did_methods/did_cheqd/src/resolution/resolver.rs +++ b/did_core/did_methods/did_cheqd/src/resolution/resolver.rs @@ -1,12 +1,16 @@ -use std::collections::HashMap; +use std::{cmp::Ordering, collections::HashMap}; use async_trait::async_trait; use bytes::Bytes; +use chrono::{DateTime, Utc}; use did_resolver::{ did_doc::schema::did_doc::DidDocument, - did_parser_nom::Did, + did_parser_nom::{Did, DidUrl}, error::GenericError, - shared_types::did_document_metadata::DidDocumentMetadata, + shared_types::{ + did_document_metadata::DidDocumentMetadata, + did_resource::{DidResource, DidResourceMetadata}, + }, traits::resolvable::{resolution_output::DidResolutionOutput, DidResolvable}, }; use http_body_util::combinators::UnsyncBoxBody; @@ -18,11 +22,15 @@ use hyper_util::{ use tokio::sync::Mutex; use tonic::{transport::Uri, Status}; +use super::transformer::CheqdResourceMetadataWithUri; use crate::{ error::{DidCheqdError, DidCheqdResult}, proto::cheqd::{ did::v2::{query_client::QueryClient as DidQueryClient, QueryDidDocRequest}, - resource::v2::query_client::QueryClient as ResourceQueryClient, + resource::v2::{ + query_client::QueryClient as ResourceQueryClient, Metadata as CheqdResourceMetadata, + QueryCollectionResourcesRequest, QueryResourceRequest, + }, }, }; @@ -83,8 +91,7 @@ type HyperClient = Client, UnsyncBoxBody, - // FUTURE - not used yet - _resources: ResourceQueryClient, + resources: ResourceQueryClient, } pub struct DidCheqdResolver { @@ -143,7 +150,7 @@ impl DidCheqdResolver { let client = CheqdGrpcClient { did: did_client, - _resources: resource_client, + resources: resource_client, }; lock.insert(network.to_owned(), client.clone()); @@ -183,6 +190,152 @@ impl DidCheqdResolver { Ok(output_builder.build()) } + + /// Resolve a cheqd DID resource & associated metadata from the given [DidUrl]. + /// Resolution is done according to the [DID-Linked Resources](https://w3c-ccg.github.io/DID-Linked-Resources/) + /// specification, however only a subset of query types are supported currently: + /// * by resource path: `did:example:/resources/` + /// * by name & type: `did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=universityDegree& + /// resourceType=anonCredsStatusList` + /// * by name & type & time: + /// `did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=universityDegree& + /// resourceType=anonCredsStatusList&versionTime=2022-08-21T08:40:00Z` + pub async fn resolve_resource(&self, url: &DidUrl) -> DidCheqdResult { + let method = url.method(); + if method != Some("cheqd") { + return Err(DidCheqdError::MethodNotSupported(format!("{method:?}"))); + } + + let network = url.namespace().unwrap_or(MAINNET_NAMESPACE); + let did_id = url + .id() + .ok_or(DidCheqdError::InvalidDidUrl(format!("missing ID {url}")))?; + + // 1. resolve by exact reference: /resources/asdf + if let Some(path) = url.path() { + let Some(resource_id) = path.strip_prefix("/resources/") else { + return Err(DidCheqdError::InvalidDidUrl(format!( + "DID Resource URL has a path without `/resources/`: {path}" + ))); + }; + + return self + .resolve_resource_by_id(did_id, resource_id, network) + .await; + } + + // 2. resolve by name & type & time (if any) + let params = url.queries(); + let resource_name = params.get("resourceName"); + let resource_type = params.get("resourceType"); + let version_time = params.get("resourceVersionTime"); + + let (Some(resource_name), Some(resource_type)) = (resource_name, resource_type) else { + return Err(DidCheqdError::InvalidDidUrl(format!( + "Resolver can only resolve by exact resource ID or name+type combination {url}" + )))?; + }; + // determine desired version_time, either from param, or *now* + let version_time = match version_time { + Some(v) => DateTime::parse_from_rfc3339(v) + .map_err(|e| DidCheqdError::InvalidDidUrl(e.to_string()))? + .to_utc(), + None => Utc::now(), + }; + + self.resolve_resource_by_name_type_and_time( + did_id, + resource_name, + resource_type, + version_time, + network, + ) + .await + } + + /// Resolve a resource from a collection (did_id) and network by an exact id. + async fn resolve_resource_by_id( + &self, + did_id: &str, + resource_id: &str, + network: &str, + ) -> DidCheqdResult { + let mut client = self.client_for_network(network).await?; + + let request = QueryResourceRequest { + collection_id: did_id.to_owned(), + id: resource_id.to_owned(), + }; + let response = client.resources.resource(request).await?; + + let query_response = response.into_inner(); + let query_response = query_response + .resource + .ok_or(DidCheqdError::InvalidResponse( + "Resource query did not return a value".into(), + ))?; + let query_resource = query_response + .resource + .ok_or(DidCheqdError::InvalidResponse( + "Resource query did not return a resource".into(), + ))?; + let query_metadata = query_response + .metadata + .ok_or(DidCheqdError::InvalidResponse( + "Resource query did not return metadata".into(), + ))?; + let metadata = DidResourceMetadata::try_from(CheqdResourceMetadataWithUri { + uri: format!( + "did:cheqd:{network}:{}/resources/{}", + query_metadata.collection_id, query_metadata.id + ), + meta: query_metadata, + })?; + + Ok(DidResource { + content: query_resource.data, + metadata, + }) + } + + /// Resolve a resource from a given collection (did_id) & network, that has a given name & type, + /// as of a given time. + async fn resolve_resource_by_name_type_and_time( + &self, + did_id: &str, + name: &str, + rtyp: &str, + time: DateTime, + network: &str, + ) -> DidCheqdResult { + let mut client = self.client_for_network(network).await?; + + let response = client + .resources + .collection_resources(QueryCollectionResourcesRequest { + collection_id: did_id.to_owned(), + // FUTURE - pagination + pagination: None, + }) + .await?; + + let query_response = response.into_inner(); + let resources = query_response.resources; + let mut filtered: Vec<_> = + filter_resources_by_name_and_type(resources.iter(), name, rtyp).collect(); + filtered.sort_by(|a, b| desc_chronological_sort_resources(a, b)); + + let resource_meta = find_resource_just_before_time(filtered.into_iter(), time); + + let Some(meta) = resource_meta else { + return Err(DidCheqdError::ResourceNotFound(format!( + "network: {network}, collection: {did_id}, name: {name}, type: {rtyp}, time: \ + {time}" + ))); + }; + + self.resolve_resource_by_id(did_id, &meta.id, network).await + } } /// Assembles a hyper client which: @@ -204,6 +357,74 @@ fn native_tls_hyper_client() -> DidCheqdResult { .build(connector)) } +/// Filter for resources which have a matching name and type +fn filter_resources_by_name_and_type<'a>( + resources: impl Iterator + 'a, + name: &'a str, + rtyp: &'a str, +) -> impl Iterator + 'a { + resources.filter(move |r| r.name == name && r.resource_type == rtyp) +} + +/// Sort resources chronologically by their created timestamps +fn desc_chronological_sort_resources( + b: &CheqdResourceMetadata, + a: &CheqdResourceMetadata, +) -> Ordering { + let (a_secs, a_ns) = a + .created + .map(|v| { + let v = v.normalized(); + (v.seconds, v.nanos) + }) + .unwrap_or((0, 0)); + let (b_secs, b_ns) = b + .created + .map(|v| { + let v = v.normalized(); + (v.seconds, v.nanos) + }) + .unwrap_or((0, 0)); + + match a_secs.cmp(&b_secs) { + Ordering::Equal => a_ns.cmp(&b_ns), + res => res, + } +} + +/// assuming `resources` is sorted by `.created` time in descending order, find +/// the resource which is closest to `before_time`, but NOT after. +/// +/// Returns a reference to this resource if it exists. +/// +/// e.g.: +/// resources: [{created: 20}, {created: 15}, {created: 10}, {created: 5}] +/// before_time: 14 +/// returns: {created: 10} +/// +/// resources: [{created: 20}, {created: 15}, {created: 10}, {created: 5}] +/// before_time: 4 +/// returns: None +fn find_resource_just_before_time<'a>( + resources: impl Iterator, + before_time: DateTime, +) -> Option<&'a CheqdResourceMetadata> { + let before_epoch = before_time.timestamp(); + + for r in resources { + let Some(created) = r.created else { + continue; + }; + + let created_epoch = created.normalized().seconds; + if created_epoch < before_epoch { + return Some(r); + } + } + + None +} + #[cfg(test)] mod unit_tests { use super::*; @@ -238,4 +459,54 @@ mod unit_tests { let e = resolver.resolve_did(&did).await.unwrap_err(); assert!(matches!(e, DidCheqdError::BadConfiguration(_))); } + + #[tokio::test] + async fn test_resolve_resource_fails_if_wrong_method() { + let url = "did:notcheqd:zF7rhDBfUt9d1gJPjx7s1J/resources/123" + .parse() + .unwrap(); + let resolver = DidCheqdResolver::new(Default::default()); + let e = resolver.resolve_resource(&url).await.unwrap_err(); + assert!(matches!(e, DidCheqdError::MethodNotSupported(_))); + } + + #[tokio::test] + async fn test_resolve_resource_fails_if_wrong_path() { + let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J/resource/123" + .parse() + .unwrap(); + let resolver = DidCheqdResolver::new(Default::default()); + let e = resolver.resolve_resource(&url).await.unwrap_err(); + assert!(matches!(e, DidCheqdError::InvalidDidUrl(_))); + } + + #[tokio::test] + async fn test_resolve_resource_fails_if_no_query() { + let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J".parse().unwrap(); + let resolver = DidCheqdResolver::new(Default::default()); + let e = resolver.resolve_resource(&url).await.unwrap_err(); + assert!(matches!(e, DidCheqdError::InvalidDidUrl(_))); + } + + #[tokio::test] + async fn test_resolve_resource_fails_if_incomplete_query() { + let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=asdf" + .parse() + .unwrap(); + let resolver = DidCheqdResolver::new(Default::default()); + let e = resolver.resolve_resource(&url).await.unwrap_err(); + assert!(matches!(e, DidCheqdError::InvalidDidUrl(_))); + } + + #[tokio::test] + async fn test_resolve_resource_fails_if_invalid_resource_time() { + // use epoch instead of XML DateTime + let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=asdf&resourceType=fdsa&\ + resourceVersionTime=12341234" + .parse() + .unwrap(); + let resolver = DidCheqdResolver::new(Default::default()); + let e = resolver.resolve_resource(&url).await.unwrap_err(); + assert!(matches!(e, DidCheqdError::InvalidDidUrl(_))); + } } diff --git a/did_core/did_methods/did_cheqd/src/resolution/transformer.rs b/did_core/did_methods/did_cheqd/src/resolution/transformer.rs index 7154d8dd96..c9ce01df37 100644 --- a/did_core/did_methods/did_cheqd/src/resolution/transformer.rs +++ b/did_core/did_methods/did_cheqd/src/resolution/transformer.rs @@ -11,15 +11,18 @@ use did_resolver::{ verification_method::{PublicKeyField, VerificationMethod, VerificationMethodType}, }, did_parser_nom::Did, - shared_types::did_document_metadata::DidDocumentMetadata, + shared_types::{did_document_metadata::DidDocumentMetadata, did_resource::DidResourceMetadata}, }; use serde_json::json; use crate::{ error::{DidCheqdError, DidCheqdResult}, - proto::cheqd::did::v2::{ - DidDoc as CheqdDidDoc, Metadata as CheqdDidDocMetadata, Service as CheqdService, - VerificationMethod as CheqdVerificationMethod, + proto::cheqd::{ + did::v2::{ + DidDoc as CheqdDidDoc, Metadata as CheqdDidDocMetadata, Service as CheqdService, + VerificationMethod as CheqdVerificationMethod, + }, + resource::v2::Metadata as CheqdResourceMetadata, }, }; @@ -204,6 +207,59 @@ impl TryFrom for DidDocumentMetadata { } } +pub(super) struct CheqdResourceMetadataWithUri { + pub uri: String, + pub meta: CheqdResourceMetadata, +} + +impl TryFrom for DidResourceMetadata { + type Error = DidCheqdError; + + fn try_from(value: CheqdResourceMetadataWithUri) -> Result { + let uri = value.uri; + let value = value.meta; + + let Some(created) = value.created else { + return Err(DidCheqdError::InvalidDidDocument(format!( + "created field missing from resource: {value:?}" + )))?; + }; + + let version = (!value.version.trim().is_empty()).then_some(value.version); + let previous_version_id = + (!value.previous_version_id.trim().is_empty()).then_some(value.previous_version_id); + let next_version_id = + (!value.next_version_id.trim().is_empty()).then_some(value.next_version_id); + + let also_known_as = value + .also_known_as + .into_iter() + .map(|aka| { + json!({ + "uri": aka.uri, + "description": aka.description + }) + }) + .collect(); + + Ok(DidResourceMetadata::builder() + .resource_uri(uri) + .resource_collection_id(value.collection_id) + .resource_id(value.id) + .resource_name(value.name) + .resource_type(value.resource_type) + .resource_version(version) + .also_known_as(Some(also_known_as)) + .media_type(value.media_type) + .created(prost_timestamp_to_dt(created)?) + .updated(None) + .checksum(value.checksum) + .previous_version_id(previous_version_id) + .next_version_id(next_version_id) + .build()) + } +} + fn prost_timestamp_to_dt(mut timestamp: prost_types::Timestamp) -> DidCheqdResult> { timestamp.normalize(); DateTime::from_timestamp(timestamp.seconds, timestamp.nanos.try_into()?).ok_or( diff --git a/did_core/did_methods/did_cheqd/tests/resolution.rs b/did_core/did_methods/did_cheqd/tests/resolution.rs index cea9a7f457..41739b8826 100644 --- a/did_core/did_methods/did_cheqd/tests/resolution.rs +++ b/did_core/did_methods/did_cheqd/tests/resolution.rs @@ -1,9 +1,9 @@ use did_cheqd::resolution::resolver::{DidCheqdResolver, DidCheqdResolverConfiguration}; use did_resolver::traits::resolvable::DidResolvable; -use serde_json::json; +use serde_json::{json, Value}; #[tokio::test] -async fn test_resolve_known_mainnet_vector() { +async fn test_resolve_known_mainnet_did_vector() { // sample from https://dev.uniresolver.io/ let did = "did:cheqd:mainnet:Ps1ysXP2Ae6GBfxNhNQNKN".parse().unwrap(); // NOTE: modifications from uni-resolver: @@ -57,7 +57,7 @@ async fn test_resolve_known_mainnet_vector() { } #[tokio::test] -async fn test_resolve_known_testnet_vector() { +async fn test_resolve_known_testnet_did_vector() { // sample from https://dev.uniresolver.io/ let did = "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" .parse() @@ -90,3 +90,78 @@ async fn test_resolve_known_testnet_vector() { assert_eq!(serde_json::to_value(doc.clone()).unwrap(), expected_doc); assert_eq!(doc, serde_json::from_value(expected_doc).unwrap()); } + +#[tokio::test] +async fn test_resolve_known_mainnet_resource_vector() { + let url = "did:cheqd:mainnet:e18756b4-25e6-42bb-b1e9-ea48cbe3c360/resources/\ + e8af40f9-3df2-40dc-b50d-d1a7e764b52d" + .parse() + .unwrap(); + + let expected_content = json!({ + "name": "Test cheqd anoncreds", + "version": "1.0", + "attrNames": ["test"] + }); + let expected_meta = json!({ + "alsoKnownAs": [{ "description": "did-url", "uri": "did:cheqd:mainnet:e18756b4-25e6-42bb-b1e9-ea48cbe3c360/resources/e8af40f9-3df2-40dc-b50d-d1a7e764b52d" }], + "resourceUri": "did:cheqd:mainnet:e18756b4-25e6-42bb-b1e9-ea48cbe3c360/resources/e8af40f9-3df2-40dc-b50d-d1a7e764b52d", + "resourceCollectionId": "e18756b4-25e6-42bb-b1e9-ea48cbe3c360", + "resourceId": "e8af40f9-3df2-40dc-b50d-d1a7e764b52d", + "resourceName": "Test cheqd anoncreds-Schema", + "resourceType": "anonCredsSchema", + "mediaType": "application/json", + "resourceVersion": "1.0", + "created": "2024-09-26T10:25:07Z", + "checksum": "01a38743e6f482c998ee8a5b84e1c7e116623a6c9b58c16125eebdf254d24da5" + }); + + let resolver = DidCheqdResolver::new(DidCheqdResolverConfiguration::default()); + let output = resolver.resolve_resource(&url).await.unwrap(); + let json_content: Value = serde_json::from_slice(&output.content).unwrap(); + assert_eq!(json_content, expected_content); + let json_meta = serde_json::to_value(output.metadata).unwrap(); + assert_eq!(json_meta, expected_meta); +} + +#[tokio::test] +async fn test_resolve_known_testnet_resource_query() { + // https://testnet-explorer.cheqd.io/transactions/222FF2D023C2C9A097BB38F3875F072DF8DEC7B0CBD46AC3459C9B4C3C74382F + + let name = "275990cc056b46176a7122cfd888f46a2bd8e3d45a71d5ff20764a874ed02edd"; + let typ = "anonCredsStatusList"; + let time = "2024-12-04T22:15:20Z"; + let url = format!( + "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b?resourceName={name}&\ + resourceType={typ}&resourceVersionTime={time}" + ) + .parse() + .unwrap(); + + let expected_content = json!({ + "currentAccumulator": "21 125DF938B3B772619CB43E561D69004CF09667376E9CD53C818D84860BAE3D1D9 21 11ECFC5F9B469AC74E2A0E329F86C6E60B423A53CAC5AE7A4DBE7A978BFFC0DA1 6 6FAD628FED470FF640BF2C5DB57C2C18D009645DBEF15D4AF710739D2AD93E2D 4 22093A3300411B059B8BB7A8C3296A2ED9C4C8E00106C3B2BAD76E25AC792063 6 71D70ECA81BCE610D1C22CADE688AF4A122C8258E8B306635A111D0A35A7238A 4 1E80F38ABA3A966B8657D722D4E956F076BB2F5CCF36AA8942E65500F8898FF3", + "revRegDefId": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/4f265d83-4657-4c37-ba80-c66cc399457e", + "revocationList": [1,1,1,0,0] + }); + let expected_meta = json!({ + "alsoKnownAs": [{ "description": "did-url", "uri": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/d08596a8-c655-45cd-88d7-ac27e8f7d183" }], + "resourceUri": "did:cheqd:testnet:8bbd2026-03f5-42c7-bf80-09f46fc4d67b/resources/d08596a8-c655-45cd-88d7-ac27e8f7d183", + "resourceCollectionId": "8bbd2026-03f5-42c7-bf80-09f46fc4d67b", + "resourceId": "d08596a8-c655-45cd-88d7-ac27e8f7d183", + "resourceName": name, + "resourceType": typ, + "mediaType": "application/json", + "resourceVersion": "1669c51f-a382-4a35-a3cc-10f6a278950e", + "created": "2024-12-04T22:15:18Z", + "checksum": "0c9b32ad86c21001fb158e0b19ef6ade10f054d8b0a63cc49f12efc46bcd6ce4", + "nextVersionId": "8e93fa1c-6ee8-4416-8aeb-8ff52cc676ab", + "previousVersionId": "942f1817-a592-44c2-b5c2-bb6579527da5" + }); + + let resolver = DidCheqdResolver::new(DidCheqdResolverConfiguration::default()); + let output = resolver.resolve_resource(&url).await.unwrap(); + let json_content: Value = serde_json::from_slice(&output.content).unwrap(); + assert_eq!(json_content, expected_content); + let json_meta = serde_json::to_value(output.metadata).unwrap(); + assert_eq!(json_meta, expected_meta); +} diff --git a/did_core/did_parser_nom/Cargo.toml b/did_core/did_parser_nom/Cargo.toml index 8b8908db41..dfc4f9eb7e 100644 --- a/did_core/did_parser_nom/Cargo.toml +++ b/did_core/did_parser_nom/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" nom = "7.1.3" serde = "1.0.160" log = "0.4.16" +percent-encoding = "2" [dev-dependencies] serde_test = "1.0.176" diff --git a/did_core/did_parser_nom/src/did_url/mod.rs b/did_core/did_parser_nom/src/did_url/mod.rs index 763f9a34f2..0f733ad6b2 100644 --- a/did_core/did_parser_nom/src/did_url/mod.rs +++ b/did_core/did_parser_nom/src/did_url/mod.rs @@ -59,8 +59,8 @@ impl DidUrl { .iter() .map(|(k, v)| { ( - self.did_url[k.clone()].to_string(), - self.did_url[v.clone()].to_string(), + query_percent_decode(&self.did_url[k.clone()]), + query_percent_decode(&self.did_url[v.clone()]), ) }) .collect() @@ -109,6 +109,14 @@ impl DidUrl { } } +/// Decode percent-encoded URL query item (application/x-www-form-urlencoded encoded). +/// Primary difference from general percent encoding is encoding of ' ' as '+' +fn query_percent_decode(input: &str) -> String { + percent_encoding::percent_decode_str(&input.replace('+', " ")) + .decode_utf8_lossy() + .into_owned() +} + impl TryFrom for DidUrl { type Error = ParseError; diff --git a/did_core/did_parser_nom/src/did_url/parsing.rs b/did_core/did_parser_nom/src/did_url/parsing.rs index cfede9c507..58a25fc648 100644 --- a/did_core/did_parser_nom/src/did_url/parsing.rs +++ b/did_core/did_parser_nom/src/did_url/parsing.rs @@ -3,11 +3,11 @@ use std::collections::HashMap; use nom::{ branch::alt, bytes::complete::{tag, take_while1}, - character::complete::{char, one_of}, + character::complete::{char, one_of, satisfy}, combinator::{all_consuming, cut, opt, recognize, success}, - multi::{many0, separated_list0}, - sequence::{preceded, separated_pair}, - IResult, + multi::{many0, many1, separated_list0}, + sequence::{preceded, separated_pair, tuple}, + AsChar, IResult, }; type UrlPart<'a> = (&'a str, Option>, Option<&'a str>); @@ -27,14 +27,29 @@ fn is_sub_delims(c: char) -> bool { "!$&'()*+,;=".contains(c) } +// pct-encoded = "%" HEXDIG HEXDIG +fn pct_encoded(input: &str) -> IResult<&str, &str> { + recognize(tuple(( + tag("%"), + satisfy(|c| c.is_hex_digit()), + satisfy(|c| c.is_hex_digit()), + )))(input) +} + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -fn is_pchar(c: char) -> bool { - is_unreserved(c) || is_sub_delims(c) || ":@".contains(c) +fn pchar(input: &str) -> IResult<&str, &str> { + alt(( + recognize(satisfy(is_unreserved)), + pct_encoded, + recognize(satisfy(is_sub_delims)), + tag(":"), + tag("@"), + ))(input) } // segment = *pchar fn segment(input: &str) -> IResult<&str, &str> { - take_while1(is_pchar)(input) + recognize(many1(pchar))(input) } // path-abempty = *( "/" segment ) @@ -44,17 +59,17 @@ fn path_abempty(input: &str) -> IResult<&str, &str> { // fragment = *( pchar / "/" / "?" ) pub(super) fn fragment_parser(input: &str) -> IResult<&str, &str> { - fn is_fragment_char(c: char) -> bool { - is_pchar(c) || "/?".contains(c) + fn fragment_element(input: &str) -> IResult<&str, &str> { + alt(((pchar), tag("/"), tag("?")))(input) } - take_while1(is_fragment_char)(input) + recognize(many1(fragment_element))(input) } // query = *( pchar / "/" / "?" ) fn query_key_value_pair(input: &str) -> IResult<&str, (&str, &str)> { - fn is_query_char(c: char) -> bool { - is_pchar(c) || "/?".contains(c) + fn query_element(input: &str) -> IResult<&str, &str> { + alt(((pchar), tag("/"), tag("?")))(input) } let (remaining, (key, value)) = cut(separated_pair( @@ -63,9 +78,9 @@ fn query_key_value_pair(input: &str) -> IResult<&str, (&str, &str)> { alt((take_while1(|c| !"&#?".contains(c)), success(""))), ))(input)?; - cut(all_consuming(take_while1(is_query_char)))(key)?; + cut(all_consuming(many1(query_element)))(key)?; if !value.is_empty() { - cut(all_consuming(take_while1(is_query_char)))(value)?; + cut(all_consuming(many1(query_element)))(value)?; } Ok((remaining, (key, value))) diff --git a/did_core/did_parser_nom/tests/did_url/negative.rs b/did_core/did_parser_nom/tests/did_url/negative.rs index d2e55533c9..d1123a4b74 100644 --- a/did_core/did_parser_nom/tests/did_url/negative.rs +++ b/did_core/did_parser_nom/tests/did_url/negative.rs @@ -32,6 +32,10 @@ test_cases_negative! { "did:example:123456789abcdefghi&query1=value1" query_invalid_char: "did:example:123456789abcdefghi?query1=v^lue1" + query_unfinished_pct_encoding: + "did:example:123456789?query=a%3&query2=b" + query_invalid_space_char: + "did:example:123456789?query=a b" relative_empty_path: "/" relative_empty_path_and_query: "/?" relative_empty_path_and_fragment: "/#" diff --git a/did_core/did_parser_nom/tests/did_url/positive.rs b/did_core/did_parser_nom/tests/did_url/positive.rs index ced17a3b35..9213bc1fdd 100644 --- a/did_core/did_parser_nom/tests/did_url/positive.rs +++ b/did_core/did_parser_nom/tests/did_url/positive.rs @@ -384,4 +384,34 @@ test_cases_positive! { ("resourceType".to_string(), "anonCredsCredDef".to_string()), ].into_iter().collect() } + test_case30: + "did:cheqd:testnet:36e695a3-f133-46ec-ac1e-79900a927f67?resourceType=anonCredsStatusList&resourceName=Example+schema-default-0&resourceVersionTime=2024-12-10T04%3A13%3A50.000Z", + Some("did:cheqd:testnet:36e695a3-f133-46ec-ac1e-79900a927f67"), + Some("cheqd"), + Some("testnet"), + Some("36e695a3-f133-46ec-ac1e-79900a927f67"), + None, + None, + { + vec![ + ("resourceName".to_string(), "Example schema-default-0".to_string()), + ("resourceType".to_string(), "anonCredsStatusList".to_string()), + ("resourceVersionTime".to_string(), "2024-12-10T04:13:50.000Z".to_string()), + ].into_iter().collect() + } + test_case31: + "did:example:123?foo+bar=123&bar%20foo=123%20123&h3%21%210%20=w%40rld%3D%3D", + Some("did:example:123"), + Some("example"), + None, + Some("123"), + None, + None, + { + vec![ + ("foo bar".to_string(), "123".to_string()), + ("bar foo".to_string(), "123 123".to_string()), + ("h3!!0 ".to_string(), "w@rld==".to_string()) + ].into_iter().collect() + } } diff --git a/did_core/did_resolver/Cargo.toml b/did_core/did_resolver/Cargo.toml index aebe69e1ed..de135ca6bb 100644 --- a/did_core/did_resolver/Cargo.toml +++ b/did_core/did_resolver/Cargo.toml @@ -13,3 +13,4 @@ async-trait = "0.1.68" chrono = { version = "0.4.24", default-features = false, features = ["serde"] } serde = { version = "1.0.160", default-features = false, features = ["derive"] } serde_json = "1.0.103" +typed-builder = "0.19.1" diff --git a/did_core/did_resolver/src/shared_types/did_resource.rs b/did_core/did_resolver/src/shared_types/did_resource.rs new file mode 100644 index 0000000000..1ba8d077bf --- /dev/null +++ b/did_core/did_resolver/src/shared_types/did_resource.rs @@ -0,0 +1,116 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use typed_builder::TypedBuilder; + +/// https://w3c-ccg.github.io/DID-Linked-Resources/ +#[derive(Clone, Debug, PartialEq, Default)] +pub struct DidResource { + pub content: Vec, + pub metadata: DidResourceMetadata, +} + +/// https://w3c-ccg.github.io/DID-Linked-Resources/ +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, TypedBuilder)] +#[serde(default)] +#[serde(rename_all = "camelCase")] +pub struct DidResourceMetadata { + // FUTURE - could be a map according to spec + /// A string or a map that conforms to the rules of RFC3986 URIs which SHOULD directly lead to + /// a location where the resource can be accessed. + /// For example: + /// did:example:46e2af9a-2ea0-4815-999d-730a6778227c/resources/ + /// 0f964a80-5d18-4867-83e3-b47f5a756f02. + pub resource_uri: String, + /// A string that conforms to a method-specific supported unique identifier format. + /// For example, a UUID: 46e2af9a-2ea0-4815-999d-730a6778227c. + pub resource_collection_id: String, + /// A string that uniquely identifies the resource. + /// For example, a UUID: 0f964a80-5d18-4867-83e3-b47f5a756f02. + pub resource_id: String, + /// A string that uniquely names and identifies a resource. This property, along with the + /// resourceType below, can be used to track version changes within a resource. + pub resource_name: String, + /// A string that identifies the type of resource. This property, along with the resourceName + /// above, can be used to track version changes within a resource. Not to be confused with + /// mediaType. + pub resource_type: String, + /// (Optional) A string that identifies the version of the resource. + /// This property is provided by the client and can be any value. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_version: Option, + /// (Optional) An array that describes alternative URIs for the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub also_known_as: Option>, + /// A string that identifies the IANA-media type of the resource. + pub media_type: String, + /// A string that identifies the time the resource was created, as an XML date-time. + #[serde(with = "xml_datetime")] + pub created: DateTime, + /// (Optional) A string that identifies the time the resource was updated, as an XML date-time. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "xml_datetime::optional")] + pub updated: Option>, + /// A string that may be used to prove that the resource has not been tampered with. + pub checksum: String, + /// (Optional) A string that identifies the previous version of the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_version_id: Option, + /// (Optional) A string that identifies the next version of the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_version_id: Option, +} + +/// Custom serialization module for XMLDateTime format. +/// Uses Z and removes subsecond precision +mod xml_datetime { + use chrono::{DateTime, SecondsFormat, Utc}; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(dt: &DateTime, serializer: S) -> Result + where + S: Serializer, + { + let s = dt.to_rfc3339_opts(SecondsFormat::Secs, true); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse::>().map_err(serde::de::Error::custom) + } + + pub mod optional { + use chrono::{DateTime, Utc}; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(dt: &Option>, serializer: S) -> Result + where + S: Serializer, + { + match dt { + Some(dt) => super::serialize(dt, serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let s = Option::::deserialize(deserializer)?; + match s { + Some(s) => { + let parsed = s + .parse::>() + .map_err(serde::de::Error::custom)?; + Ok(Some(parsed)) + } + None => Ok(None), + } + } + } +} diff --git a/did_core/did_resolver/src/shared_types/mod.rs b/did_core/did_resolver/src/shared_types/mod.rs index 4073fd7ad9..cd45b70522 100644 --- a/did_core/did_resolver/src/shared_types/mod.rs +++ b/did_core/did_resolver/src/shared_types/mod.rs @@ -1,2 +1,3 @@ pub mod did_document_metadata; +pub mod did_resource; pub mod media_type; diff --git a/justfile b/justfile index c8894595e3..2df483e9cb 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,7 @@ check-aries-vcx-anoncreds: cargo test --manifest-path="aries/aries_vcx/Cargo.toml" -F askar_wallet,anoncreds --tests test-unit test_name="": - RUST_TEST_THREADS=1 cargo test --workspace --lib --exclude aries-vcx-agent --exclude mediator {{test_name}} -F did_doc/jwk -F public_key/jwk + RUST_TEST_THREADS=1 cargo test --workspace --lib --exclude aries-vcx-agent --exclude mediator {{test_name}} -F did_doc/jwk -F public_key/jwk -F aries_vcx_ledger/cheqd test-integration-aries-vcx features test_name="": cargo test --manifest-path="aries/aries_vcx/Cargo.toml" -F {{features}} -- --ignored {{test_name}} @@ -26,5 +26,8 @@ test-integration-aries-vcx features test_name="": test-integration-aries-vcx-vdrproxy test_name="": cargo test --manifest-path="aries/aries_vcx/Cargo.toml" -F vdr_proxy_ledger,anoncreds -- --ignored {{test_name}} +test-integration-aries-vcx-ledger: + cargo test --manifest-path="aries/aries_vcx_ledger/Cargo.toml" -F cheqd + test-integration-did-crate test_name="": cargo test --examples -p did_doc -p did_parser_nom -p did_resolver -p did_resolver_registry -p did_resolver_sov -p did_resolver_web -p did_key -p did_peer -p did_jwk -p did_cheqd -F did_doc/jwk --test "*" \ No newline at end of file