From db6f585c44f6bcd9dc2ab3bb8a9423aec8482899 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:32:51 +0100 Subject: [PATCH 01/18] refactor(dataverse): expose components from vc mod --- .../okp4-dataverse/src/credential/crypto.rs | 4 ++-- .../okp4-dataverse/src/credential/mod.rs | 2 +- .../src/credential/rdf_marker.rs | 5 ++-- contracts/okp4-dataverse/src/credential/vc.rs | 24 +++++++++---------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/crypto.rs b/contracts/okp4-dataverse/src/credential/crypto.rs index 91647c33..fea90bd7 100644 --- a/contracts/okp4-dataverse/src/credential/crypto.rs +++ b/contracts/okp4-dataverse/src/credential/crypto.rs @@ -39,7 +39,7 @@ impl From<(CanonicalizationAlg, DigestAlg, SignatureAlg)> for CryptoSuite { impl CryptoSuite { pub fn verify_document( &self, - deps: DepsMut<'_>, + deps: &'_ DepsMut<'_>, unsecured_doc: &[Quad<'_>], proof_opts: &[Quad<'_>], proof_value: &[u8], @@ -77,7 +77,7 @@ impl CryptoSuite { fn verify( &self, - deps: DepsMut<'_>, + deps: &'_ DepsMut<'_>, message: &[u8], signature: &[u8], pub_key: &[u8], diff --git a/contracts/okp4-dataverse/src/credential/mod.rs b/contracts/okp4-dataverse/src/credential/mod.rs index bd34dcf1..7f634366 100644 --- a/contracts/okp4-dataverse/src/credential/mod.rs +++ b/contracts/okp4-dataverse/src/credential/mod.rs @@ -1,5 +1,5 @@ mod crypto; pub mod error; mod proof; -mod rdf_marker; +pub mod rdf_marker; pub mod vc; diff --git a/contracts/okp4-dataverse/src/credential/rdf_marker.rs b/contracts/okp4-dataverse/src/credential/rdf_marker.rs index 8b7e7d17..7e7550d3 100644 --- a/contracts/okp4-dataverse/src/credential/rdf_marker.rs +++ b/contracts/okp4-dataverse/src/credential/rdf_marker.rs @@ -10,9 +10,8 @@ pub const RDF_DATE_TYPE: NamedNode<'_> = NamedNode { iri: "http://www.w3.org/2001/XMLSchema#dateTime", }; -pub const VC_RDF_TYPE: Term<'_> = Term::NamedNode(NamedNode { - iri: "https://www.w3.org/2018/credentials#VerifiableCredential", -}); +pub const IRI_VC_TYPE: &str = "https://www.w3.org/2018/credentials#VerifiableCredential"; +pub const VC_RDF_TYPE: Term<'_> = Term::NamedNode(NamedNode { iri: IRI_VC_TYPE }); pub const VC_RDF_ISSUER: NamedNode<'_> = NamedNode { iri: "https://www.w3.org/2018/credentials#issuer", }; diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index fbb53ebf..eda36cb9 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -9,21 +9,21 @@ use rio_api::model::{BlankNode, Literal, NamedNode, Subject, Term}; #[derive(Debug, PartialEq)] pub struct VerifiableCredential<'a> { - id: &'a str, - types: Vec<&'a str>, - issuer: &'a str, - issuance_date: &'a str, - expiration_date: Option<&'a str>, - claims: Vec>, - status: Option>, - proof: Vec>, + pub id: &'a str, + pub types: Vec<&'a str>, + pub issuer: &'a str, + pub issuance_date: &'a str, + pub expiration_date: Option<&'a str>, + pub claims: Vec>, + pub status: Option>, + pub proof: Vec>, unsecured_document: Dataset<'a>, } #[derive(Debug, PartialEq)] pub struct Claim<'a> { - id: &'a str, - content: Dataset<'a>, + pub id: &'a str, + pub content: Dataset<'a>, } #[derive(Debug, PartialEq)] @@ -74,7 +74,7 @@ impl<'a> TryFrom<&'a Dataset<'a>> for VerifiableCredential<'a> { } impl<'a> VerifiableCredential<'a> { - pub fn verify(&self, deps: DepsMut<'_>) -> Result<(), VerificationError> { + pub fn verify(&self, deps: &'_ DepsMut<'_>) -> Result<(), VerificationError> { let proof = self .proof .iter() @@ -347,7 +347,7 @@ mod test { let owned_quads = testutil::read_test_quads(case); let dataset = Dataset::from(owned_quads.as_slice()); let vc = VerifiableCredential::try_from(&dataset).unwrap(); - let verif_res = vc.verify(deps.as_mut()); + let verif_res = vc.verify(&deps.as_mut()); assert!(verif_res.is_ok()); } } From 0292eb59bb373be648d3b326ac1d02b41f7e3b43 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:33:32 +0100 Subject: [PATCH 02/18] chore(dataverse): add cognitarium client dep --- Cargo.lock | 1 + contracts/okp4-dataverse/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0dd5a9d8..5cee1660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -779,6 +779,7 @@ dependencies = [ "itertools 0.12.1", "multibase", "okp4-cognitarium", + "okp4-cognitarium-client", "okp4-rdf", "rio_api", "rio_turtle", diff --git a/contracts/okp4-dataverse/Cargo.toml b/contracts/okp4-dataverse/Cargo.toml index e4e3fcde..46c9a07b 100644 --- a/contracts/okp4-dataverse/Cargo.toml +++ b/contracts/okp4-dataverse/Cargo.toml @@ -38,6 +38,7 @@ cw-utils.workspace = true cw2.workspace = true itertools = "0.12.1" multibase = "0.9.1" +okp4-cognitarium-client.workspace = true okp4-cognitarium.workspace = true okp4-rdf.workspace = true rio_api.workspace = true From 33474559727da5cc5991d732ddf87bbc00ca20ce Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:33:55 +0100 Subject: [PATCH 03/18] feat(dataverse): introduce dataverse credential model --- .../src/registrar/credential.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 contracts/okp4-dataverse/src/registrar/credential.rs diff --git a/contracts/okp4-dataverse/src/registrar/credential.rs b/contracts/okp4-dataverse/src/registrar/credential.rs new file mode 100644 index 00000000..3f462872 --- /dev/null +++ b/contracts/okp4-dataverse/src/registrar/credential.rs @@ -0,0 +1,67 @@ +use crate::credential::rdf_marker::IRI_VC_TYPE; +use crate::credential::vc::VerifiableCredential; +use crate::ContractError; +use cosmwasm_std::Addr; +use itertools::Itertools; +use okp4_rdf::dataset::Dataset; + +#[derive(Debug, PartialEq)] +pub struct DataverseCredential<'a> { + pub submitter_addr: Addr, + pub id: &'a str, + pub issuer: &'a str, + pub r#type: &'a str, + pub valid_from: &'a str, + pub valid_until: Option<&'a str>, + pub subject: &'a str, + pub claim: &'a Dataset<'a>, +} + +impl<'a> DataverseCredential<'a> { + fn extract_vc_type(vc: &'a VerifiableCredential<'a>) -> Result<&'a str, ContractError> { + vc.types + .iter() + .filter(|t| *t != &IRI_VC_TYPE) + .exactly_one() + .map_err(|_| { + ContractError::UnsupportedCredential( + "credential is expected to have exactly one type".to_string(), + ) + }) + .map(|t| *t) + } + + fn extract_vc_claim( + vc: &'a VerifiableCredential<'a>, + ) -> Result<(&'a str, &'a Dataset<'a>), ContractError> { + vc.claims + .iter() + .exactly_one() + .map(|claim| (claim.id, &(claim.content))) + .map_err(|_| { + ContractError::UnsupportedCredential( + "credential is expected to contain exactly one claim".to_string(), + ) + }) + } +} + +impl<'a> TryFrom<(Addr, &'a VerifiableCredential<'a>)> for DataverseCredential<'a> { + type Error = ContractError; + + fn try_from( + (submitter_addr, vc): (Addr, &'a VerifiableCredential<'a>), + ) -> Result { + let (subject, claim) = DataverseCredential::extract_vc_claim(vc)?; + Ok(DataverseCredential { + submitter_addr, + id: vc.id, + issuer: vc.issuer, + r#type: DataverseCredential::extract_vc_type(vc)?, + valid_from: vc.issuance_date, + valid_until: vc.expiration_date, + subject, + claim, + }) + } +} From d4da31d4945b3c303a620d7a42b8b65e0958deb8 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:34:13 +0100 Subject: [PATCH 04/18] feat(dataverse): impl rdf serialization of dataverse cred --- contracts/okp4-dataverse/src/registrar/rdf.rs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 contracts/okp4-dataverse/src/registrar/rdf.rs diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs new file mode 100644 index 00000000..159af395 --- /dev/null +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -0,0 +1,123 @@ +use crate::credential::rdf_marker::{RDF_DATE_TYPE, RDF_TYPE}; +use crate::registrar::credential::DataverseCredential; +use crate::ContractError; +use cosmwasm_std::{Binary, StdError}; +use okp4_rdf::serde::{DataFormat, TripleWriter}; +use rio_api::model::{BlankNode, Literal, NamedNode, Subject, Term, Triple}; + +pub const VC_SUBMITTER_ADDRESS: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#submitterAddress", +}; +pub const VC_ISSUER: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#issuer", +}; +pub const VC_VALID_FROM: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#validFrom", +}; +pub const VC_VALID_UNTIL: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#validUntil", +}; +pub const VC_SUBJECT: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#subject", +}; +pub const VC_CLAIM: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#claim", +}; + +impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { + type Error = ContractError; + + fn try_from(credential: &'a DataverseCredential<'a>) -> Result { + let c_subject = Subject::NamedNode(NamedNode { iri: credential.id }); + let claim_node = BlankNode { id: "c0" }; + + let mut triples = vec![ + Triple { + subject: c_subject, + predicate: VC_SUBMITTER_ADDRESS, + object: Term::NamedNode(NamedNode { + iri: credential.submitter_addr.as_str(), + }), + }, + Triple { + subject: c_subject, + predicate: VC_ISSUER, + object: Term::NamedNode(NamedNode { + iri: credential.issuer, + }), + }, + Triple { + subject: c_subject, + predicate: VC_VALID_FROM, + object: Term::Literal(Literal::Typed { + value: credential.valid_from, + datatype: RDF_DATE_TYPE, + }), + }, + Triple { + subject: c_subject, + predicate: RDF_TYPE, + object: Term::NamedNode(NamedNode { + iri: credential.r#type, + }), + }, + Triple { + subject: c_subject, + predicate: VC_SUBJECT, + object: Term::NamedNode(NamedNode { + iri: credential.subject, + }), + }, + Triple { + subject: c_subject, + predicate: VC_CLAIM, + object: Term::BlankNode(claim_node), + }, + ]; + + triples.extend(credential.claim.iter().map(|q| { + let subject = match q.subject { + Subject::NamedNode(n) if n.iri == credential.subject => { + Subject::BlankNode(claim_node) + } + _ => q.subject, + }; + Triple { + subject, + predicate: q.predicate, + object: q.object, + } + })); + + if let Some(valid_until) = credential.valid_until { + triples.push(Triple { + subject: c_subject, + predicate: VC_VALID_UNTIL, + object: Term::Literal(Literal::Typed { + value: valid_until, + datatype: RDF_DATE_TYPE, + }), + }); + } + + Ok(triples) + } +} + +pub fn serialize( + credential: &DataverseCredential<'_>, + format: DataFormat, +) -> Result { + let triples: Vec> = credential.try_into()?; + let out: Vec = Vec::default(); + let mut writer = TripleWriter::new(&format, out); + for triple in triples { + writer + .write(&triple) + .map_err(|e| StdError::serialize_err("triple", format!("Error writing triple: {e}")))?; + } + + Ok(Binary::from(writer.finish().map_err(|e| { + StdError::serialize_err("triple", format!("Error writing triple: {e}")) + })?)) +} From 385426ee413750bd8f414c761f432d3ae784c78d Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:34:42 +0100 Subject: [PATCH 05/18] feat(dataverse): implements claims registering in cognitarium --- contracts/okp4-dataverse/src/registrar/mod.rs | 3 + .../okp4-dataverse/src/registrar/registry.rs | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 contracts/okp4-dataverse/src/registrar/mod.rs create mode 100644 contracts/okp4-dataverse/src/registrar/registry.rs diff --git a/contracts/okp4-dataverse/src/registrar/mod.rs b/contracts/okp4-dataverse/src/registrar/mod.rs new file mode 100644 index 00000000..767ae316 --- /dev/null +++ b/contracts/okp4-dataverse/src/registrar/mod.rs @@ -0,0 +1,3 @@ +pub mod credential; +mod rdf; +pub mod registry; diff --git a/contracts/okp4-dataverse/src/registrar/registry.rs b/contracts/okp4-dataverse/src/registrar/registry.rs new file mode 100644 index 00000000..72260733 --- /dev/null +++ b/contracts/okp4-dataverse/src/registrar/registry.rs @@ -0,0 +1,65 @@ +use crate::registrar::credential::DataverseCredential; +use crate::registrar::rdf::serialize; +use crate::state::DATAVERSE; +use crate::ContractError; +use cosmwasm_std::{DepsMut, StdResult, Storage, WasmMsg}; +use okp4_cognitarium::msg::{ + DataFormat, Node, SelectItem, SelectQuery, SimpleWhereCondition, TriplePattern, VarOrNode, + VarOrNodeOrLiteral, WhereCondition, IRI, +}; +use okp4_cognitarium_client::CognitariumClient; + +/// ClaimRegistrar is the entity responsible to manage claims (i.e. submission and revocation) into +/// the Dataverse, ensuring that any pre-condition criteria to an action is met, and any attached +/// logic is properly executed. +pub struct ClaimRegistrar { + triplestore: CognitariumClient, +} + +impl ClaimRegistrar { + const RDF_DATA_FORMAT: DataFormat = DataFormat::NTriples; + + pub fn try_new(storage: &dyn Storage) -> StdResult { + let dataverse = DATAVERSE.load(storage)?; + Ok(Self { + triplestore: CognitariumClient::new(dataverse.triplestore_address), + }) + } + + pub fn submit_claim( + &self, + deps: &DepsMut<'_>, + credential: &DataverseCredential<'_>, + ) -> Result { + let resp = self.triplestore.select( + deps.querier, + SelectQuery { + prefixes: vec![], + limit: Some(1u32), + select: vec![SelectItem::Variable("p".to_string())], + r#where: vec![WhereCondition::Simple(SimpleWhereCondition::TriplePattern( + TriplePattern { + subject: VarOrNode::Node(Node::NamedNode(IRI::Full( + credential.id.to_string(), + ))), + predicate: VarOrNode::Variable("p".to_string()), + object: VarOrNodeOrLiteral::Variable("o".to_string()), + }, + ))], + }, + )?; + + if !resp.results.bindings.is_empty() { + Err(ContractError::CredentialAlreadyExists( + credential.id.to_string(), + ))?; + } + + self.triplestore + .insert_data( + Some(Self::RDF_DATA_FORMAT), + serialize(credential, (&Self::RDF_DATA_FORMAT).into())?, + ) + .map_err(ContractError::from) + } +} From 6abb559566a24942f9d888f94c6fdf39b2388af7 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:35:07 +0100 Subject: [PATCH 06/18] feat(dataverse): properly persists claims in related exec msg --- contracts/okp4-dataverse/src/contract.rs | 24 +++++++++++++++++++----- contracts/okp4-dataverse/src/error.rs | 8 +++++++- contracts/okp4-dataverse/src/lib.rs | 1 + 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/contracts/okp4-dataverse/src/contract.rs b/contracts/okp4-dataverse/src/contract.rs index 53ba357d..ca206386 100644 --- a/contracts/okp4-dataverse/src/contract.rs +++ b/contracts/okp4-dataverse/src/contract.rs @@ -67,14 +67,14 @@ pub fn instantiate( pub fn execute( deps: DepsMut<'_>, _env: Env, - _info: MessageInfo, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { ExecuteMsg::SubmitClaims { metadata, format: _, - } => execute::submit_claims(deps, metadata), + } => execute::submit_claims(deps, info, metadata), _ => Err(StdError::generic_err("Not implemented").into()), } } @@ -82,19 +82,33 @@ pub fn execute( pub mod execute { use super::*; use crate::credential::vc::VerifiableCredential; + use crate::registrar::credential::DataverseCredential; + use crate::registrar::registry::ClaimRegistrar; use okp4_rdf::dataset::Dataset; use okp4_rdf::serde::NQuadsReader; use std::io::BufReader; - pub fn submit_claims(deps: DepsMut<'_>, data: Binary) -> Result { + pub fn submit_claims( + deps: DepsMut<'_>, + info: MessageInfo, + data: Binary, + ) -> Result { let buf = BufReader::new(data.as_slice()); let mut reader = NQuadsReader::new(buf); let rdf_quads = reader.read_all()?; let vc_dataset = Dataset::from(rdf_quads.as_slice()); let vc = VerifiableCredential::try_from(&vc_dataset)?; - vc.verify(deps)?; + vc.verify(&deps)?; + + let credential = DataverseCredential::try_from((info.sender, &vc))?; + let registrar = ClaimRegistrar::try_new(deps.storage)?; - Ok(Response::default()) + Ok(Response::default() + .add_attribute("action", "submit_claims") + .add_attribute("credential", credential.id) + .add_attribute("subject", credential.subject) + .add_attribute("type", credential.r#type) + .add_message(registrar.submit_claim(&deps, &credential)?)) } } diff --git a/contracts/okp4-dataverse/src/error.rs b/contracts/okp4-dataverse/src/error.rs index 1e62f163..f0df3721 100644 --- a/contracts/okp4-dataverse/src/error.rs +++ b/contracts/okp4-dataverse/src/error.rs @@ -17,6 +17,12 @@ pub enum ContractError { #[error("Invalid credential: '{0}'")] InvalidCredential(#[from] InvalidCredentialError), - #[error("Credential verification failed: {0}")] + #[error("Credential verification failed: '{0}'")] CredentialVerification(#[from] VerificationError), + + #[error("Credential not supported: '{0}'")] + UnsupportedCredential(String), + + #[error("Credential already exists: '{0}'")] + CredentialAlreadyExists(String), } diff --git a/contracts/okp4-dataverse/src/lib.rs b/contracts/okp4-dataverse/src/lib.rs index 300fb02f..6ea2e762 100644 --- a/contracts/okp4-dataverse/src/lib.rs +++ b/contracts/okp4-dataverse/src/lib.rs @@ -14,6 +14,7 @@ pub mod contract; mod credential; mod error; pub mod msg; +mod registrar; pub mod state; mod testutil; From 1489f95e8305c5fed943c69fe54353b16c5e6995 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:35:41 +0100 Subject: [PATCH 07/18] feat(dataverse): rework SubmitClaims msg doc --- contracts/okp4-dataverse/src/msg.rs | 19 +++++++++++++------ docs/okp4-dataverse.md | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/contracts/okp4-dataverse/src/msg.rs b/contracts/okp4-dataverse/src/msg.rs index 0516c20a..4197f68f 100644 --- a/contracts/okp4-dataverse/src/msg.rs +++ b/contracts/okp4-dataverse/src/msg.rs @@ -26,11 +26,7 @@ pub enum ExecuteMsg { /// /// #### Format /// - /// Claims are injected into the dataverse through Verifiable Presentations (VPs). These presentations effectively amalgamate and - /// showcase multiple credentials, thus providing a cohesive and comprehensive view of the assertions being made. - /// - /// While the data in a VP typically revolves around a common subject, it accommodates an unlimited number of subjects and issuers. - /// This flexibility allows for a broad spectrum of claims to be represented. + /// Claims are injected into the dataverse through Verifiable Credentials (VCs). /// /// Primarily, the claims leverage the OKP4 ontology, which facilitates articulating assertions about widely acknowledged resources /// in the dataverse, including digital services, digital resources, zones, governance, and more. @@ -41,11 +37,22 @@ pub enum ExecuteMsg { /// /// To maintain integrity and coherence in the dataverse, several preconditions are set for the submission of claims: /// - /// 1. **Format Requirement**: Claims must be encapsulated within Verifiable Presentations (VPs). + /// 1. **Format Requirement**: Claims must be encapsulated within Verifiable Credentials (VCs). /// /// 2. **Unique Identifier Mandate**: Each Verifiable Credential within the dataverse must possess a unique identifier. /// /// 3. **Issuer Signature**: Claims must bear the issuer's signature. This signature must be verifiable, ensuring authenticity and credibility. + /// + /// 4. **Content**: The actual implementation supports the submission of a single Verifiable Credential, containing a single claim. + /// + /// #### Supported cryptographic proofs + /// + /// - `Ed25519Signature2020` + /// + /// - `EcdsaSecp256k1Signature2019` + /// + /// - `DataIntegrity` with the following cryptosuites: `eddsa-2022`, `eddsa-rdfc-2022`. + /// SubmitClaims { /// The serialized metadata intended for attachment. /// This metadata should adhere to the format specified in the `format` field. diff --git a/docs/okp4-dataverse.md b/docs/okp4-dataverse.md index 17f2d896..8dd93cca 100644 --- a/docs/okp4-dataverse.md +++ b/docs/okp4-dataverse.md @@ -68,9 +68,7 @@ The SubmitClaims message is a pivotal component in the dataverse, enabling entit #### Format -Claims are injected into the dataverse through Verifiable Presentations (VPs). These presentations effectively amalgamate and showcase multiple credentials, thus providing a cohesive and comprehensive view of the assertions being made. - -While the data in a VP typically revolves around a common subject, it accommodates an unlimited number of subjects and issuers. This flexibility allows for a broad spectrum of claims to be represented. +Claims are injected into the dataverse through Verifiable Credentials (VCs). Primarily, the claims leverage the OKP4 ontology, which facilitates articulating assertions about widely acknowledged resources in the dataverse, including digital services, digital resources, zones, governance, and more. @@ -80,12 +78,22 @@ Additionally, other schemas may also be employed to supplement and enhance the v To maintain integrity and coherence in the dataverse, several preconditions are set for the submission of claims: -1. **Format Requirement**: Claims must be encapsulated within Verifiable Presentations (VPs). +1. **Format Requirement**: Claims must be encapsulated within Verifiable Credentials (VCs). 2. **Unique Identifier Mandate**: Each Verifiable Credential within the dataverse must possess a unique identifier. 3. **Issuer Signature**: Claims must bear the issuer's signature. This signature must be verifiable, ensuring authenticity and credibility. +4. **Content**: The actual implementation supports the submission of a single Verifiable Credential, containing a single claim. + +#### Supported cryptographic proofs + +- `Ed25519Signature2020` + +- `EcdsaSecp256k1Signature2019` + +- `DataIntegrity` with the following cryptosuites: `eddsa-2022`, `eddsa-rdfc-2022`. + | parameter | description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `submit_claims` | _(Required.) _ **object**. | @@ -213,5 +221,5 @@ let b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ``` --- -*Rendered by [Fadroma](https://fadroma.tech) ([@fadroma/schema 1.1.0](https://www.npmjs.com/package/@fadroma/schema)) from `okp4-dataverse.json` (`f38b5aafa47fb333`)* +*Rendered by [Fadroma](https://fadroma.tech) ([@fadroma/schema 1.1.0](https://www.npmjs.com/package/@fadroma/schema)) from `okp4-dataverse.json` (`30443a4cdcde9c27`)* ```` From f74aa3b3aecfc4c81ebd2787fe905ae8c1e02a86 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Tue, 13 Feb 2024 21:47:05 +0100 Subject: [PATCH 08/18] feat(dataverse): use a dedicated type predicate for cred storage --- contracts/okp4-dataverse/src/registrar/rdf.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs index 159af395..03685cca 100644 --- a/contracts/okp4-dataverse/src/registrar/rdf.rs +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -1,4 +1,4 @@ -use crate::credential::rdf_marker::{RDF_DATE_TYPE, RDF_TYPE}; +use crate::credential::rdf_marker::RDF_DATE_TYPE; use crate::registrar::credential::DataverseCredential; use crate::ContractError; use cosmwasm_std::{Binary, StdError}; @@ -8,6 +8,9 @@ use rio_api::model::{BlankNode, Literal, NamedNode, Subject, Term, Triple}; pub const VC_SUBMITTER_ADDRESS: NamedNode<'_> = NamedNode { iri: "dataverse:credential#submitterAddress", }; +pub const VC_TYPE: NamedNode<'_> = NamedNode { + iri: "dataverse:credential#type", +}; pub const VC_ISSUER: NamedNode<'_> = NamedNode { iri: "dataverse:credential#issuer", }; @@ -29,6 +32,7 @@ impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { fn try_from(credential: &'a DataverseCredential<'a>) -> Result { let c_subject = Subject::NamedNode(NamedNode { iri: credential.id }); + //todo: use the canon identifier issuer instead and rename all blank nodes let claim_node = BlankNode { id: "c0" }; let mut triples = vec![ @@ -48,17 +52,17 @@ impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { }, Triple { subject: c_subject, - predicate: VC_VALID_FROM, - object: Term::Literal(Literal::Typed { - value: credential.valid_from, - datatype: RDF_DATE_TYPE, + predicate: VC_TYPE, + object: Term::NamedNode(NamedNode { + iri: credential.r#type, }), }, Triple { subject: c_subject, - predicate: RDF_TYPE, - object: Term::NamedNode(NamedNode { - iri: credential.r#type, + predicate: VC_VALID_FROM, + object: Term::Literal(Literal::Typed { + value: credential.valid_from, + datatype: RDF_DATE_TYPE, }), }, Triple { From b2f6d43ce3b32e4a917987e59622c1073f80ec0b Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:03:53 +0100 Subject: [PATCH 09/18] feat(dataverse): garantee claim storage format --- .../src/registrar/credential.rs | 51 +++++++++++++++---- contracts/okp4-dataverse/src/registrar/rdf.rs | 33 +++--------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/contracts/okp4-dataverse/src/registrar/credential.rs b/contracts/okp4-dataverse/src/registrar/credential.rs index 3f462872..4e094240 100644 --- a/contracts/okp4-dataverse/src/registrar/credential.rs +++ b/contracts/okp4-dataverse/src/registrar/credential.rs @@ -1,9 +1,10 @@ use crate::credential::rdf_marker::IRI_VC_TYPE; use crate::credential::vc::VerifiableCredential; +use crate::registrar::rdf::VC_CLAIM; use crate::ContractError; use cosmwasm_std::Addr; use itertools::Itertools; -use okp4_rdf::dataset::Dataset; +use rio_api::model::{BlankNode, NamedNode, Subject, Term, Triple}; #[derive(Debug, PartialEq)] pub struct DataverseCredential<'a> { @@ -14,7 +15,7 @@ pub struct DataverseCredential<'a> { pub valid_from: &'a str, pub valid_until: Option<&'a str>, pub subject: &'a str, - pub claim: &'a Dataset<'a>, + pub claim: Vec>, } impl<'a> DataverseCredential<'a> { @@ -33,16 +34,46 @@ impl<'a> DataverseCredential<'a> { fn extract_vc_claim( vc: &'a VerifiableCredential<'a>, - ) -> Result<(&'a str, &'a Dataset<'a>), ContractError> { - vc.claims + ) -> Result<(&'a str, Vec>), ContractError> { + //todo: use the canon identifier issuer instead and rename all blank nodes + let claim_node = BlankNode { id: "c0" }; + + let claim = vc.claims.iter().exactly_one().map_err(|_| { + ContractError::UnsupportedCredential( + "credential is expected to contain exactly one claim".to_string(), + ) + })?; + + let mut triples = claim + .content .iter() - .exactly_one() - .map(|claim| (claim.id, &(claim.content))) - .map_err(|_| { - ContractError::UnsupportedCredential( - "credential is expected to contain exactly one claim".to_string(), - ) + .map(|q| { + let subject = match q.subject { + Subject::NamedNode(n) => { + if n.iri != claim.id { + Err(ContractError::UnsupportedCredential( + "claim hierarchy can be forge only through blank nodes".to_string(), + ))?; + } + Subject::BlankNode(claim_node) + } + _ => q.subject, + }; + Ok(Triple { + subject, + predicate: q.predicate, + object: q.object, + }) }) + .collect::>, ContractError>>()?; + + triples.push(Triple { + subject: Subject::NamedNode(NamedNode { iri: vc.id }), + predicate: VC_CLAIM, + object: Term::BlankNode(BlankNode { id: "c0" }), + }); + + Ok((claim.id, triples)) } } diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs index 03685cca..42160981 100644 --- a/contracts/okp4-dataverse/src/registrar/rdf.rs +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -3,7 +3,7 @@ use crate::registrar::credential::DataverseCredential; use crate::ContractError; use cosmwasm_std::{Binary, StdError}; use okp4_rdf::serde::{DataFormat, TripleWriter}; -use rio_api::model::{BlankNode, Literal, NamedNode, Subject, Term, Triple}; +use rio_api::model::{Literal, NamedNode, Subject, Term, Triple}; pub const VC_SUBMITTER_ADDRESS: NamedNode<'_> = NamedNode { iri: "dataverse:credential#submitterAddress", @@ -27,13 +27,9 @@ pub const VC_CLAIM: NamedNode<'_> = NamedNode { iri: "dataverse:credential#claim", }; -impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { - type Error = ContractError; - - fn try_from(credential: &'a DataverseCredential<'a>) -> Result { +impl<'a> From<&'a DataverseCredential<'a>> for Vec> { + fn from(credential: &'a DataverseCredential<'a>) -> Self { let c_subject = Subject::NamedNode(NamedNode { iri: credential.id }); - //todo: use the canon identifier issuer instead and rename all blank nodes - let claim_node = BlankNode { id: "c0" }; let mut triples = vec![ Triple { @@ -72,26 +68,9 @@ impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { iri: credential.subject, }), }, - Triple { - subject: c_subject, - predicate: VC_CLAIM, - object: Term::BlankNode(claim_node), - }, ]; - triples.extend(credential.claim.iter().map(|q| { - let subject = match q.subject { - Subject::NamedNode(n) if n.iri == credential.subject => { - Subject::BlankNode(claim_node) - } - _ => q.subject, - }; - Triple { - subject, - predicate: q.predicate, - object: q.object, - } - })); + triples.extend(credential.claim.as_slice()); if let Some(valid_until) = credential.valid_until { triples.push(Triple { @@ -104,7 +83,7 @@ impl<'a> TryFrom<&'a DataverseCredential<'a>> for Vec> { }); } - Ok(triples) + triples } } @@ -112,7 +91,7 @@ pub fn serialize( credential: &DataverseCredential<'_>, format: DataFormat, ) -> Result { - let triples: Vec> = credential.try_into()?; + let triples: Vec> = credential.into(); let out: Vec = Vec::default(); let mut writer = TripleWriter::new(&format, out); for triple in triples { From 88141ba85e51c365cbfa7f4704d5dc1c6118f833 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:38:22 +0100 Subject: [PATCH 10/18] fix(dataverse): properly extract claims from VC --- contracts/okp4-dataverse/src/credential/vc.rs | 7 +--- packages/okp4-rdf/src/dataset.rs | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index eda36cb9..ea78fd11 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -217,12 +217,7 @@ impl<'a> VerifiableCredential<'a> { }) .map_ok(|claim_id| Claim { id: claim_id.iri, - content: Dataset::new( - dataset - .match_pattern(Some(claim_id.into()), None, None, None) - .copied() - .collect(), - ), + content: dataset.sub_graph(claim_id.into()), }) .collect() } diff --git a/packages/okp4-rdf/src/dataset.rs b/packages/okp4-rdf/src/dataset.rs index ef2d38bf..e7d4e414 100644 --- a/packages/okp4-rdf/src/dataset.rs +++ b/packages/okp4-rdf/src/dataset.rs @@ -1,6 +1,7 @@ use crate::owned_model::OwnedQuad; use itertools::Itertools; use rio_api::model::{GraphName, NamedNode, Quad, Subject, Term}; +use std::collections::HashSet; use std::slice::Iter; #[derive(Clone, Debug, PartialEq)] @@ -49,6 +50,39 @@ impl<'a> Dataset<'a> { ) -> QuadPatternFilter<'a, Iter<'a, Quad<'a>>> { self.iter().skip_pattern((s, p, o, g).into()) } + + pub fn sub_graph(&'a self, subject: Subject<'a>) -> Dataset<'a> { + Self::new(Self::sub_graph_from_quads(self.as_ref(), HashSet::new(), subject).0) + } + + fn sub_graph_from_quads( + quads: &'a [Quad<'a>], + mut visited: HashSet>, + subject: Subject<'a>, + ) -> (Vec>, HashSet>) { + let mut sub_graph = vec![]; + for quad in quads + .iter() + .match_pattern((Some(subject), None, None, None).into()) + { + sub_graph.push(*quad); + + let maybe_node: Option> = match quad.object { + Term::NamedNode(n) => Some(n.into()), + Term::BlankNode(n) => Some(n.into()), + _ => None, + }; + + if let Some(s) = maybe_node.filter(|n| !visited.contains(n)) { + visited.insert(subject); + let (new_quads, new_visited) = Self::sub_graph_from_quads(quads, visited, s); + visited = new_visited; + sub_graph.extend(new_quads); + } + } + + (sub_graph, visited) + } } #[derive(Copy, Clone)] From 2914238008e085db214ea11939d3cf3c1d05fb2c Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:38:38 +0100 Subject: [PATCH 11/18] test(dataverse): add credential mapping tests --- .../src/registrar/credential.rs | 82 +++++++++++++++++++ .../testdata/vc-unsupported-1.nq | 13 +++ .../testdata/vc-unsupported-2.nq | 15 ++++ .../testdata/vc-unsupported-3.nq | 16 ++++ .../testdata/vc-unsupported-4.nq | 16 ++++ contracts/okp4-dataverse/testdata/vc-valid.nq | 14 ++++ 6 files changed, 156 insertions(+) create mode 100644 contracts/okp4-dataverse/testdata/vc-unsupported-1.nq create mode 100644 contracts/okp4-dataverse/testdata/vc-unsupported-2.nq create mode 100644 contracts/okp4-dataverse/testdata/vc-unsupported-3.nq create mode 100644 contracts/okp4-dataverse/testdata/vc-unsupported-4.nq create mode 100644 contracts/okp4-dataverse/testdata/vc-valid.nq diff --git a/contracts/okp4-dataverse/src/registrar/credential.rs b/contracts/okp4-dataverse/src/registrar/credential.rs index 4e094240..be95b4ad 100644 --- a/contracts/okp4-dataverse/src/registrar/credential.rs +++ b/contracts/okp4-dataverse/src/registrar/credential.rs @@ -96,3 +96,85 @@ impl<'a> TryFrom<(Addr, &'a VerifiableCredential<'a>)> for DataverseCredential<' }) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::testutil::testutil; + use okp4_rdf::dataset::Dataset; + use rio_api::model::Literal; + + #[test] + fn proper_from_verifiable() { + let owned_quads = testutil::read_test_quads("vc-valid.nq"); + let dataset = Dataset::from(owned_quads.as_slice()); + let vc = VerifiableCredential::try_from(&dataset).unwrap(); + let dc_res = DataverseCredential::try_from(( + Addr::unchecked("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf"), + &vc, + )); + + assert!(dc_res.is_ok()); + assert_eq!(dc_res.unwrap(), DataverseCredential { + submitter_addr: Addr::unchecked("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf"), + id: "https://w3id.org/okp4/ontology/vnext/schema/credential/digital-service/description/72cab400-5bd6-4eb4-8605-a5ee8c1a45c9", + issuer: "did:key:zQ3shs7auhJSmVJpiUbQWco6bxxEhSqWnVEPvaBHBRvBKw6Q3", + r#type: "https://w3id.org/okp4/ontology/vnext/schema/credential/digital-service/description/DigitalServiceDescriptionCredential", + valid_from: "2024-01-22T00:00:00", + valid_until: Some("2025-01-22T00:00:00"), + subject: "did:key:zQ3shhb4SvzBRLbBonsvKb3WX6WoDeKWHpsXXXMhAJETqXAfB", + claim: vec![Triple { + subject: BlankNode {id: "c0"}.into(), + predicate: NamedNode {iri: "https://w3id.org/okp4/ontology/vnext/schema/credential/digital-service/description/hasCategory"}, + object: NamedNode {iri: "https://w3id.org/okp4/ontology/vnext/thesaurus/digital-service-category/Storage"}.into(), + },Triple { + subject: BlankNode {id: "c0"}.into(), + predicate: NamedNode {iri: "https://w3id.org/okp4/ontology/vnext/schema/credential/digital-service/description/hasTag"}, + object: Literal::Simple {value: "Cloud"}.into(), + },Triple { + subject: NamedNode {iri: "https://w3id.org/okp4/ontology/vnext/schema/credential/digital-service/description/72cab400-5bd6-4eb4-8605-a5ee8c1a45c9"}.into(), + predicate: NamedNode {iri: "dataverse:credential#claim"}, + object: BlankNode {id: "c0"}.into(), + }], + }) + } + + #[test] + fn unsupported_from_verifiable() { + let cases = vec![ + ( + "vc-unsupported-1.nq", + "credential is expected to have exactly one type", + ), + ( + "vc-unsupported-2.nq", + "credential is expected to have exactly one type", + ), + ( + "vc-unsupported-3.nq", + "credential is expected to contain exactly one claim", + ), + ( + "vc-unsupported-4.nq", + "claim hierarchy can be forge only through blank nodes", + ), + ]; + + for case in cases { + let owned_quads = testutil::read_test_quads(case.0); + let dataset = Dataset::from(owned_quads.as_slice()); + let vc = VerifiableCredential::try_from(&dataset).unwrap(); + let dc_res = DataverseCredential::try_from(( + Addr::unchecked("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf"), + &vc, + )); + + assert!(dc_res.is_err()); + if let ContractError::UnsupportedCredential(msg) = dc_res.err().unwrap() { + assert_eq!(msg, case.1.to_string()); + } else { + assert!(false); + } + } + } +} diff --git a/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq b/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq new file mode 100644 index 00000000..203b301a --- /dev/null +++ b/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq @@ -0,0 +1,13 @@ + . + "Cloud" . + . + . + "2024-01-22T00:00:00"^^ . + "2025-01-22T00:00:00"^^ . + . + _:b0 . +_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . +_:b1 _:b0 . +_:b1 _:b0 . +_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 _:b0 . diff --git a/contracts/okp4-dataverse/testdata/vc-unsupported-2.nq b/contracts/okp4-dataverse/testdata/vc-unsupported-2.nq new file mode 100644 index 00000000..db6e7132 --- /dev/null +++ b/contracts/okp4-dataverse/testdata/vc-unsupported-2.nq @@ -0,0 +1,15 @@ + . + "Cloud" . + . + . + . + . + "2024-01-22T00:00:00"^^ . + "2025-01-22T00:00:00"^^ . + . + _:b0 . +_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . +_:b1 _:b0 . +_:b1 _:b0 . +_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 _:b0 . diff --git a/contracts/okp4-dataverse/testdata/vc-unsupported-3.nq b/contracts/okp4-dataverse/testdata/vc-unsupported-3.nq new file mode 100644 index 00000000..d13c7438 --- /dev/null +++ b/contracts/okp4-dataverse/testdata/vc-unsupported-3.nq @@ -0,0 +1,16 @@ + . + "Cloud" . + "Cloud" . + . + . + . + . + "2024-01-22T00:00:00"^^ . + "2025-01-22T00:00:00"^^ . + . + _:b0 . +_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . +_:b1 _:b0 . +_:b1 _:b0 . +_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 _:b0 . diff --git a/contracts/okp4-dataverse/testdata/vc-unsupported-4.nq b/contracts/okp4-dataverse/testdata/vc-unsupported-4.nq new file mode 100644 index 00000000..a18311fc --- /dev/null +++ b/contracts/okp4-dataverse/testdata/vc-unsupported-4.nq @@ -0,0 +1,16 @@ + . + "Cloud" . + . + "yes I am" . + . + . + . + "2024-01-22T00:00:00"^^ . + "2025-01-22T00:00:00"^^ . + . + _:b0 . +_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . +_:b1 _:b0 . +_:b1 _:b0 . +_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 _:b0 . diff --git a/contracts/okp4-dataverse/testdata/vc-valid.nq b/contracts/okp4-dataverse/testdata/vc-valid.nq new file mode 100644 index 00000000..cbdd23a2 --- /dev/null +++ b/contracts/okp4-dataverse/testdata/vc-valid.nq @@ -0,0 +1,14 @@ + . + "Cloud" . + . + . + . + "2024-01-22T00:00:00"^^ . + "2025-01-22T00:00:00"^^ . + . + _:b0 . +_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . +_:b1 _:b0 . +_:b1 _:b0 . +_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 _:b0 . From db8a835c960b546734c935cc6097506e4a0be7e6 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:51:34 +0100 Subject: [PATCH 12/18] test(dataverse): cover credential serialization to cogni --- contracts/okp4-dataverse/src/registrar/rdf.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs index 42160981..bf44a589 100644 --- a/contracts/okp4-dataverse/src/registrar/rdf.rs +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -104,3 +104,42 @@ pub fn serialize( StdError::serialize_err("triple", format!("Error writing triple: {e}")) })?)) } + +#[cfg(test)] +mod test { + use super::*; + use crate::credential::vc::VerifiableCredential; + use crate::testutil::testutil; + use cosmwasm_std::Addr; + use okp4_rdf::dataset::Dataset; + + #[test] + fn proper_serialization() { + let owned_quads = testutil::read_test_quads("vc-valid.nq"); + let dataset = Dataset::from(owned_quads.as_slice()); + let vc = VerifiableCredential::try_from(&dataset).unwrap(); + let dc = DataverseCredential::try_from(( + Addr::unchecked("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf"), + &vc, + )) + .unwrap(); + + let expected = " . + . + . + \"2024-01-22T00:00:00\"^^ . + . +_:c0 . +_:c0 \"Cloud\" . + _:c0 . + \"2025-01-22T00:00:00\"^^ .\n"; + + let serialization_res = serialize(&dc, DataFormat::NQuads); + assert!(serialization_res.is_ok()); + + assert_eq!( + String::from_utf8(serialization_res.unwrap().0).unwrap(), + expected + ); + } +} From 1c27613105138eb69781f9b04198c0f11f745975 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:14:06 +0100 Subject: [PATCH 13/18] test(dataverse): enhance vc proper parsing test --- contracts/okp4-dataverse/src/credential/vc.rs | 53 ++++++++++++++++--- .../testdata/vc-eddsa-2020-ok-unsecured.nq | 13 +++-- .../testdata/vc-eddsa-2020-ok.nq | 19 ++++--- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index ea78fd11..74f6489e 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -294,6 +294,7 @@ mod test { use super::*; use crate::testutil::testutil; use cosmwasm_std::testing::mock_dependencies; + use rio_api::model::Quad; #[test] fn proper_vc_from_dataset() { @@ -306,22 +307,62 @@ mod test { let vc_res = VerifiableCredential::try_from(&dataset); assert!(vc_res.is_ok()); let vc = vc_res.unwrap(); - assert_eq!(vc.id, "http://example.edu/credentials/58473"); + assert_eq!(vc.id, "http://example.edu/credentials/3732"); assert_eq!( vc.types, - vec!["https://www.w3.org/2018/credentials#VerifiableCredential"] + vec![ + "https://example.org/examples#UniversityDegreeCredential", + "https://www.w3.org/2018/credentials#VerifiableCredential" + ] ); assert_eq!( vc.issuer, "did:key:z6MkpwdnLPAm4apwcrRYQ6fZ3rAcqjLZR4AMk14vimfnozqY" ); - assert_eq!(vc.issuance_date, "2023-05-01T06:09:10Z"); - assert_eq!(vc.expiration_date, None); + assert_eq!(vc.issuance_date, "2024-02-16T00:00:00Z"); + assert_eq!(vc.expiration_date, Some("2026-02-16T00:00:00Z")); assert_eq!( vc.claims, vec![Claim { - id: "did:key:z6MkpwdnLPAm4apwcrRYQ6fZ3rAcqjLZR4AMk14vimfnozqY", - content: Dataset::new(vec![]), + id: "did:key:zDnaeUm3QkcyZWZTPttxB711jgqRDhkwvhF485SFw1bDZ9AQw", + content: Dataset::new(vec![ + Quad { + subject: NamedNode { + iri: "did:key:zDnaeUm3QkcyZWZTPttxB711jgqRDhkwvhF485SFw1bDZ9AQw" + } + .into(), + predicate: NamedNode { + iri: "https://example.org/examples#degree" + }, + object: BlankNode { id: "b2" }.into(), + graph_name: None + }, + Quad { + subject: BlankNode { id: "b2" }.into(), + predicate: NamedNode { + iri: "http://schema.org/name" + }, + object: Literal::Typed { + value: "Bachelor of Science and Arts", + datatype: NamedNode { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML" + } + } + .into(), + graph_name: None + }, + Quad { + subject: BlankNode { id: "b2" }.into(), + predicate: NamedNode { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + }, + object: NamedNode { + iri: "https://example.org/examples#BachelorDegree" + } + .into(), + graph_name: None + } + ]) }] ); assert_eq!(vc.status, None); diff --git a/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok-unsecured.nq b/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok-unsecured.nq index 73caed51..dbe80a33 100644 --- a/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok-unsecured.nq +++ b/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok-unsecured.nq @@ -1,4 +1,9 @@ - . - . - "2023-05-01T06:09:10Z"^^ . - . + _:b2 . + . + . + . + "2026-02-16T00:00:00Z"^^ . + "2024-02-16T00:00:00Z"^^ . + . +_:b2 "Bachelor of Science and Arts"^^ . +_:b2 . diff --git a/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok.nq b/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok.nq index 451e978b..4c0a3964 100644 --- a/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok.nq +++ b/contracts/okp4-dataverse/testdata/vc-eddsa-2020-ok.nq @@ -1,10 +1,15 @@ - . - _:b0 . - . - "2023-05-01T06:09:10Z"^^ . - . -_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . + _:b2 . + . + . + _:b0 . + . + "2026-02-16T00:00:00Z"^^ . + "2024-02-16T00:00:00Z"^^ . + . +_:b1 "2024-02-16T17:35:56.668169Z"^^ _:b0 . _:b1 _:b0 . _:b1 _:b0 . -_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 "zUuTPsT5aKs53ciMY6qEj2dqZxK4XnLoZhX26amB9GMCMhfcTmLbtndcW5JS4gUqPkxGxsCmZCKuvkFnDgrGFrWD"^^ _:b0 . _:b1 _:b0 . +_:b2 "Bachelor of Science and Arts"^^ . +_:b2 . From dcab990abc1a370dc282a3003fb5b7850bf281da Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:14:20 +0100 Subject: [PATCH 14/18] test(dataverse): cover contract proper submit claim --- contracts/okp4-dataverse/src/contract.rs | 121 ++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/contracts/okp4-dataverse/src/contract.rs b/contracts/okp4-dataverse/src/contract.rs index ca206386..498bf65a 100644 --- a/contracts/okp4-dataverse/src/contract.rs +++ b/contracts/okp4-dataverse/src/contract.rs @@ -122,11 +122,16 @@ pub mod query {} #[cfg(test)] mod tests { use super::*; - use crate::msg::{TripleStoreConfig, TripleStoreLimitsInput}; + use crate::msg::{RdfFormat, TripleStoreConfig, TripleStoreLimitsInput}; + use crate::testutil::testutil::read_test_data; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ - Addr, Attribute, ContractResult, HexBinary, SubMsg, SystemError, SystemResult, Uint128, - Uint64, WasmQuery, + from_json, Addr, Attribute, ContractResult, CosmosMsg, HexBinary, SubMsg, SystemError, + SystemResult, Uint128, Uint64, WasmQuery, + }; + use okp4_cognitarium::msg::{ + DataFormat, Head, Node, Results, SelectItem, SelectQuery, SelectResponse, + SimpleWhereCondition, TriplePattern, VarOrNode, VarOrNodeOrLiteral, WhereCondition, IRI, }; #[test] @@ -189,4 +194,114 @@ mod tests { } ) } + + #[test] + fn proper_submit_claims() { + let mut deps = mock_dependencies(); + deps.querier.update_wasm(|query| match query { + WasmQuery::Smart { contract_addr, msg } => { + if contract_addr != "my-dataverse-addr" { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.to_string(), + }); + } + let query_msg: StdResult = from_json(msg); + assert_eq!( + query_msg, + Ok(okp4_cognitarium::msg::QueryMsg::Select { + query: SelectQuery { + prefixes: vec![], + limit: Some(1u32), + select: vec![SelectItem::Variable("p".to_string())], + r#where: vec![WhereCondition::Simple( + SimpleWhereCondition::TriplePattern(TriplePattern { + subject: VarOrNode::Node(Node::NamedNode(IRI::Full( + "http://example.edu/credentials/3732".to_string(), + ))), + predicate: VarOrNode::Variable("p".to_string()), + object: VarOrNodeOrLiteral::Variable("o".to_string()), + }) + )], + } + }) + ); + + let select_resp = SelectResponse { + results: Results { bindings: vec![] }, + head: Head { vars: vec![] }, + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&select_resp).unwrap())) + } + _ => SystemResult::Err(SystemError::Unknown {}), + }); + + DATAVERSE + .save( + deps.as_mut().storage, + &Dataverse { + name: "my-dataverse".to_string(), + triplestore_address: Addr::unchecked("my-dataverse-addr"), + }, + ) + .unwrap(); + + let resp = execute( + deps.as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary(read_test_data("vc-eddsa-2020-ok.nq")), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.messages.len(), 1); + assert_eq!( + resp.attributes, + vec![ + Attribute::new("action", "submit_claims"), + Attribute::new("credential", "http://example.edu/credentials/3732"), + Attribute::new( + "subject", + "did:key:zDnaeUm3QkcyZWZTPttxB711jgqRDhkwvhF485SFw1bDZ9AQw" + ), + Attribute::new( + "type", + "https://example.org/examples#UniversityDegreeCredential" + ), + ] + ); + + let expected_data = " . + . + . + \"2024-02-16T00:00:00Z\"^^ . + . +_:c0 _:b2 . +_:b2 \"Bachelor of Science and Arts\"^^ . +_:b2 . + _:c0 . + \"2026-02-16T00:00:00Z\"^^ .\n"; + + match resp.messages[0].msg.clone() { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) if contract_addr == "my-dataverse-addr".to_string() && funds == vec![] => { + let exec_msg: StdResult = from_json(msg); + assert!(exec_msg.is_ok()); + match exec_msg.unwrap() { + okp4_cognitarium::msg::ExecuteMsg::InsertData { format, data } => { + assert_eq!(format, Some(DataFormat::NTriples)); + assert_eq!(String::from_utf8(data.0).unwrap(), expected_data); + } + _ => assert!(false), + } + } + _ => assert!(false), + } + } } From 6c56cd096478f17cee80592c3e640aaf8ac87293 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:38:50 +0100 Subject: [PATCH 15/18] feat(dataverse): allow no proof in vc parsing according to spec --- contracts/okp4-dataverse/src/credential/error.rs | 3 --- contracts/okp4-dataverse/src/credential/vc.rs | 4 ---- 2 files changed, 7 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/error.rs b/contracts/okp4-dataverse/src/credential/error.rs index 161ac07e..85b87da3 100644 --- a/contracts/okp4-dataverse/src/credential/error.rs +++ b/contracts/okp4-dataverse/src/credential/error.rs @@ -12,9 +12,6 @@ pub enum InvalidCredentialError { #[error("Missing issuance date")] MissingIssuanceDate, - #[error("Missing proof, at least a supported one")] - MissingProof, - #[error("Invalid proof: {0}")] InvalidProof(#[from] InvalidProofError), diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index 74f6489e..06b96b3f 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -42,10 +42,6 @@ impl<'a> TryFrom<&'a Dataset<'a>> for VerifiableCredential<'a> { let (proofs, proof_graphs): (Vec>, Vec>) = Self::extract_proofs(dataset, id)?.into_iter().unzip(); - if proofs.is_empty() { - return Err(InvalidCredentialError::MissingProof); - } - let mut unsecured_filter: Vec> = proof_graphs .into_iter() .map(|g| (None, None, None, Some(Some(g.into()))).into()) From 087c93386d64b1f2c4bfd8eb3a7364f2244a880b Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:39:15 +0100 Subject: [PATCH 16/18] fix(dataverse): correct some error messages in vc parsing --- contracts/okp4-dataverse/src/credential/vc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index 06b96b3f..c03e84a2 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -95,10 +95,10 @@ impl<'a> VerifiableCredential<'a> { .subjects() .exactly_one() .map_err(|e| match e.size_hint() { - (_, Some(_)) => InvalidCredentialError::MissingIdentifier, - _ => InvalidCredentialError::Malformed( + (_, Some(_)) => InvalidCredentialError::Malformed( "Credential cannot have more than one id".to_string(), ), + _ => InvalidCredentialError::MissingIdentifier, }) .and_then(|s| match s { Subject::NamedNode(n) => Ok(n), From 963a81ca87ccfae0479ca7f2fe617a42fe71d1e4 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:39:37 +0100 Subject: [PATCH 17/18] test(dataverse): cover submit claims msg err cases --- contracts/okp4-dataverse/src/contract.rs | 124 +++++++++++++++++- .../testdata/vc-unsupported-1.nq | 21 +-- 2 files changed, 134 insertions(+), 11 deletions(-) diff --git a/contracts/okp4-dataverse/src/contract.rs b/contracts/okp4-dataverse/src/contract.rs index 498bf65a..cb6ca550 100644 --- a/contracts/okp4-dataverse/src/contract.rs +++ b/contracts/okp4-dataverse/src/contract.rs @@ -131,8 +131,10 @@ mod tests { }; use okp4_cognitarium::msg::{ DataFormat, Head, Node, Results, SelectItem, SelectQuery, SelectResponse, - SimpleWhereCondition, TriplePattern, VarOrNode, VarOrNodeOrLiteral, WhereCondition, IRI, + SimpleWhereCondition, TriplePattern, Value, VarOrNode, VarOrNodeOrLiteral, WhereCondition, + IRI, }; + use std::collections::BTreeMap; #[test] fn proper_instantiate() { @@ -304,4 +306,124 @@ _:b2 assert!(false), } } + + #[test] + fn submit_nonrdf_claims() { + let resp = execute( + mock_dependencies().as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary("notrdf".as_bytes().to_vec()), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_err()); + assert!(matches!(resp.err().unwrap(), ContractError::ParseRDF(_))) + } + + #[test] + fn submit_invalid_claims() { + let resp = execute( + mock_dependencies().as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary(vec![]), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_err()); + assert!(matches!( + resp.err().unwrap(), + ContractError::InvalidCredential(_) + )) + } + + #[test] + fn submit_unverified_claims() { + let resp = execute( + mock_dependencies().as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary(read_test_data("vc-eddsa-2020-ok-unsecured.nq")), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_err()); + assert!(matches!( + resp.err().unwrap(), + ContractError::CredentialVerification(_) + )) + } + + #[test] + fn submit_unsupported_claims() { + let resp = execute( + mock_dependencies().as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary(read_test_data("vc-unsupported-1.nq")), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_err()); + assert!(matches!( + resp.err().unwrap(), + ContractError::UnsupportedCredential(_) + )) + } + + #[test] + fn submit_existing_claims() { + let mut deps = mock_dependencies(); + deps.querier.update_wasm(|query| match query { + WasmQuery::Smart { .. } => { + let select_resp = SelectResponse { + results: Results { + bindings: vec![BTreeMap::from([( + "p".to_string(), + Value::BlankNode { + value: "".to_string(), + }, + )])], + }, + head: Head { vars: vec![] }, + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&select_resp).unwrap())) + } + _ => SystemResult::Err(SystemError::Unknown {}), + }); + + DATAVERSE + .save( + deps.as_mut().storage, + &Dataverse { + name: "my-dataverse".to_string(), + triplestore_address: Addr::unchecked("my-dataverse-addr"), + }, + ) + .unwrap(); + + let resp = execute( + deps.as_mut(), + mock_env(), + mock_info("okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf", &[]), + ExecuteMsg::SubmitClaims { + metadata: Binary(read_test_data("vc-eddsa-2020-ok.nq")), + format: Some(RdfFormat::NQuads), + }, + ); + + assert!(resp.is_err()); + assert!( + matches!(resp.err().unwrap(), ContractError::CredentialAlreadyExists(id) if id == "http://example.edu/credentials/3732") + ); + } } diff --git a/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq b/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq index 203b301a..6b709c96 100644 --- a/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq +++ b/contracts/okp4-dataverse/testdata/vc-unsupported-1.nq @@ -1,13 +1,14 @@ - . - "Cloud" . - . - . - "2024-01-22T00:00:00"^^ . - "2025-01-22T00:00:00"^^ . - . - _:b0 . -_:b1 "2024-02-01T17:46:53.676947Z"^^ _:b0 . + _:b2 . + . + _:b0 . + . + "2026-02-16T00:00:00Z"^^ . + "2024-02-16T00:00:00Z"^^ . + . +_:b1 "2024-02-17T13:32:14.814613Z"^^ _:b0 . _:b1 _:b0 . _:b1 _:b0 . -_:b1 "z3WboEDRwsWokH8vQrveVWbg6fQnqhHfhrkGHT9tyG2GYgzQVZ9zFW6eK2ZNcnGhydqXWDwwTsZq29e7cHJkbnVkF"^^ _:b0 . +_:b1 "z5vGstniEMfyk5riV1UQvFXhXzdcbZ1978JFGdn1H2wTdp6qvxqDuw5xg8M33hjdZTG6zGuCbAhgCqf7R2CkhVRp5"^^ _:b0 . _:b1 _:b0 . +_:b2 "Bachelor of Science and Arts"^^ . +_:b2 . From bd4a1697ce6f7ddd37f227430a66d6a6e3d7da6d Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:57:32 +0100 Subject: [PATCH 18/18] fix(dataverse): persists claim sender addr as literal --- contracts/okp4-dataverse/src/contract.rs | 2 +- contracts/okp4-dataverse/src/registrar/rdf.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/okp4-dataverse/src/contract.rs b/contracts/okp4-dataverse/src/contract.rs index cb6ca550..84cfa8da 100644 --- a/contracts/okp4-dataverse/src/contract.rs +++ b/contracts/okp4-dataverse/src/contract.rs @@ -276,7 +276,7 @@ mod tests { ] ); - let expected_data = " . + let expected_data = " \"okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf\" . . . \"2024-02-16T00:00:00Z\"^^ . diff --git a/contracts/okp4-dataverse/src/registrar/rdf.rs b/contracts/okp4-dataverse/src/registrar/rdf.rs index bf44a589..ebdfb01a 100644 --- a/contracts/okp4-dataverse/src/registrar/rdf.rs +++ b/contracts/okp4-dataverse/src/registrar/rdf.rs @@ -35,8 +35,8 @@ impl<'a> From<&'a DataverseCredential<'a>> for Vec> { Triple { subject: c_subject, predicate: VC_SUBMITTER_ADDRESS, - object: Term::NamedNode(NamedNode { - iri: credential.submitter_addr.as_str(), + object: Term::Literal(Literal::Simple { + value: credential.submitter_addr.as_str(), }), }, Triple { @@ -124,7 +124,7 @@ mod test { )) .unwrap(); - let expected = " . + let expected = " \"okp41072nc6egexqr2v6vpp7yxwm68plvqnkf6xsytf\" . . . \"2024-01-22T00:00:00\"^^ .