From a97228457c616bdd518155ec2f04aa914607d3d3 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:27:52 +0100 Subject: [PATCH] feat(dataverse): implements VC rdf parsing --- contracts/okp4-dataverse/Cargo.toml | 2 + contracts/okp4-dataverse/src/did/consts.rs | 33 +++ contracts/okp4-dataverse/src/did/mod.rs | 3 + contracts/okp4-dataverse/src/did/vc.rs | 262 +++++++++++++++++++++ contracts/okp4-dataverse/src/error.rs | 3 + contracts/okp4-dataverse/src/lib.rs | 1 + 6 files changed, 304 insertions(+) create mode 100644 contracts/okp4-dataverse/src/did/consts.rs create mode 100644 contracts/okp4-dataverse/src/did/mod.rs create mode 100644 contracts/okp4-dataverse/src/did/vc.rs diff --git a/contracts/okp4-dataverse/Cargo.toml b/contracts/okp4-dataverse/Cargo.toml index c20f23ef..3991a239 100644 --- a/contracts/okp4-dataverse/Cargo.toml +++ b/contracts/okp4-dataverse/Cargo.toml @@ -36,6 +36,8 @@ cw-utils.workspace = true cw2.workspace = true itertools = "0.12.1" okp4-cognitarium.workspace = true +okp4-rdf.workspace = true +rio_api.workspace = true schemars.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/contracts/okp4-dataverse/src/did/consts.rs b/contracts/okp4-dataverse/src/did/consts.rs new file mode 100644 index 00000000..c71c4109 --- /dev/null +++ b/contracts/okp4-dataverse/src/did/consts.rs @@ -0,0 +1,33 @@ +use rio_api::model::{NamedNode, Term}; + +pub const RDF_TYPE: NamedNode<'_> = NamedNode { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", +}; +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 VC_RDF_ISSUER: NamedNode<'_> = NamedNode { + iri: "https://www.w3.org/2018/credentials#issuer", +}; +pub const VC_RDF_ISSUANCE_DATE: NamedNode<'_> = NamedNode { + iri: "https://www.w3.org/2018/credentials#issuanceDate", +}; + +pub const VC_RDF_EXPIRATION_DATE: NamedNode<'_> = NamedNode { + iri: "https://www.w3.org/2018/credentials#expirationDate", +}; + +pub const VC_RDF_CREDENTIAL_SUBJECT: NamedNode<'_> = NamedNode { + iri: "https://www.w3.org/2018/credentials#credentialSubject", +}; + +pub const VC_RDF_CREDENTIAL_STATUS: NamedNode<'_> = NamedNode { + iri: "https://www.w3.org/2018/credentials#credentialStatus", +}; + +pub const VC_RDF_PROOF: NamedNode<'_> = NamedNode { + iri: "https://w3id.org/security#proof", +}; diff --git a/contracts/okp4-dataverse/src/did/mod.rs b/contracts/okp4-dataverse/src/did/mod.rs new file mode 100644 index 00000000..86f9aad3 --- /dev/null +++ b/contracts/okp4-dataverse/src/did/mod.rs @@ -0,0 +1,3 @@ +mod consts; +mod crypto; +mod vc; diff --git a/contracts/okp4-dataverse/src/did/vc.rs b/contracts/okp4-dataverse/src/did/vc.rs new file mode 100644 index 00000000..94f77b7d --- /dev/null +++ b/contracts/okp4-dataverse/src/did/vc.rs @@ -0,0 +1,262 @@ +use crate::did::consts::*; +use crate::did::crypto::Proof; +use crate::ContractError; +use itertools::Itertools; +use okp4_rdf::dataset::QuadIterator; +use okp4_rdf::dataset::{Dataset, QuadPattern}; +use rio_api::model::{BlankNode, Literal, NamedNode, Subject, Term}; + +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>, + unsecured_document: Dataset<'a>, +} + +pub struct Claim<'a> { + id: &'a str, + content: Dataset<'a>, +} + +pub struct Status<'a> { + id: &'a str, + type_: &'a str, + content: Dataset<'a>, +} + +impl<'a> TryFrom<&'a Dataset<'a>> for VerifiableCredential<'a> { + type Error = ContractError; + + fn try_from(dataset: &'a Dataset<'a>) -> Result { + let id = Self::extract_identifier(&dataset)?; + + let mut proofs = vec![]; + let mut unsecured_filter: Vec> = vec![]; + for (proof, graph) in Self::extract_proofs(dataset, id)? { + proofs.push(proof); + unsecured_filter.push((None, None, None, Some(Some(graph.into()))).into()) + } + + Ok(Self { + id: id.iri, + types: Self::extract_types(dataset, id)?, + issuer: Self::extract_issuer(&dataset, id)?.iri, + issuance_date: Self::extract_issuance_date(&dataset, id)?, + expiration_date: Self::extract_expiration_date(&dataset, id)?, + claims: Self::extract_claims(dataset, id)?, + status: Self::extract_status(dataset, id)?, + proof: proofs, + unsecured_document: Dataset::new( + dataset + .into_iter() + .skip_patterns(unsecured_filter) + .map(|quad| *quad) + .collect(), + ), + }) + } +} + +impl<'a> VerifiableCredential<'a> { + fn extract_identifier(dataset: &'a Dataset<'a>) -> Result, ContractError> { + dataset + .match_pattern(None, Some(RDF_TYPE), Some(VC_RDF_TYPE), None) + .subjects() + .exactly_one() + .map_err(|_| { + ContractError::InvalidCredential( + "Credential must contains one identifier".to_string(), + ) + }) + .and_then(|s| match s { + Subject::NamedNode(n) => Ok(n), + _ => Err(ContractError::InvalidCredential( + "Credential identifier must be a named node".to_string(), + )), + }) + } + + fn extract_types( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(RDF_TYPE), None, None) + .objects() + .map(|o| match o { + Term::NamedNode(n) => Ok(n.iri), + _ => Err(ContractError::InvalidCredential( + "Credential type must be a named node".to_string(), + )), + }) + .collect() + } + + fn extract_issuer( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_ISSUER), None, None) + .objects() + .exactly_one() + .map_err(|_| { + ContractError::InvalidCredential("Credential must contains one issuer".to_string()) + }) + .and_then(|o| match o { + Term::NamedNode(n) => Ok(n), + _ => Err(ContractError::InvalidCredential( + "Credential issuer must be a named node".to_string(), + )), + }) + } + + fn extract_issuance_date( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result<&'a str, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_ISSUANCE_DATE), None, None) + .objects() + .exactly_one() + .map_err(|_| { + ContractError::InvalidCredential( + "Credential must contains one issuance date".to_string(), + ) + }) + .and_then(|o| match o { + Term::Literal(Literal::Typed { value, datatype }) if datatype == RDF_DATE_TYPE => { + Ok(value) + } + _ => Err(ContractError::InvalidCredential( + "Credential issuance date must be a date".to_string(), + )), + }) + } + + fn extract_expiration_date( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_EXPIRATION_DATE), None, None) + .objects() + .at_most_one() + .map_err(|_| { + ContractError::InvalidCredential( + "Credential may contains one expiration date".to_string(), + ) + }) + .and_then(|o| match o { + Some(t) => match t { + Term::Literal(Literal::Typed { value, datatype }) + if datatype == RDF_DATE_TYPE => + { + Ok(Some(value)) + } + _ => Err(ContractError::InvalidCredential( + "Credential expiration date must be a date".to_string(), + )), + }, + None => Ok(None), + }) + } + + fn extract_claims( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result>, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_CREDENTIAL_SUBJECT), None, None) + .objects() + .map(|claim_id| match claim_id { + Term::NamedNode(n) => Ok(n), + _ => Err(ContractError::InvalidCredential( + "Credential claim ids must be named nodes".to_string(), + )), + }) + .map_ok(|claim_id| Claim { + id: claim_id.iri, + content: Dataset::new( + dataset + .match_pattern(Some(claim_id.into()), None, None, None) + .map(|quad| *quad) + .collect(), + ), + }) + .collect() + } + + fn extract_status( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result>, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_CREDENTIAL_STATUS), None, None) + .objects() + .at_most_one() + .map_err(|_| { + ContractError::InvalidCredential( + "Credential can contains at most one status".to_string(), + ) + }) + .and_then(|maybe_term| match maybe_term { + Some(term) => match term { + Term::NamedNode(n) => Ok(Some(Status { + id: n.iri, + type_: Self::extract_types(dataset, n)? + .iter() + .exactly_one() + .map_err(|_| { + ContractError::InvalidCredential( + "Credential status can only have one type".to_string(), + ) + })?, + content: Dataset::new( + dataset + .match_pattern(Some(n.into()), None, None, None) + .map(|quad| *quad) + .collect(), + ), + })), + _ => Err(ContractError::InvalidCredential( + "Credential status id must be a named node".to_string(), + )), + }, + None => Ok(None), + }) + } + + fn extract_proofs( + dataset: &'a Dataset<'a>, + id: NamedNode<'a>, + ) -> Result, BlankNode<'a>)>, ContractError> { + dataset + .match_pattern(Some(id.into()), Some(VC_RDF_PROOF), None, None) + .objects() + .map(|o| { + match o { + Term::BlankNode(n) => Ok(n), + _ => Err(ContractError::InvalidCredential( + "Credential proof must be encapsulated in blank node graph names" + .to_string(), + )), + } + .and_then(|g| { + Proof::try_from(Dataset::new( + dataset + .match_pattern(None, None, None, Some(Some(g.into()))) + .map(|quad| *quad) + .collect(), + )) + .map(|p| (p, g)) + }) + }) + .collect() + } +} diff --git a/contracts/okp4-dataverse/src/error.rs b/contracts/okp4-dataverse/src/error.rs index 21651b09..5ef10253 100644 --- a/contracts/okp4-dataverse/src/error.rs +++ b/contracts/okp4-dataverse/src/error.rs @@ -8,4 +8,7 @@ pub enum ContractError { #[error("{0}")] Instantiate2Address(#[from] Instantiate2AddressError), + + #[error("Invalid credential: '{0}'")] + InvalidCredential(String), } diff --git a/contracts/okp4-dataverse/src/lib.rs b/contracts/okp4-dataverse/src/lib.rs index 14159c9a..b85d0f00 100644 --- a/contracts/okp4-dataverse/src/lib.rs +++ b/contracts/okp4-dataverse/src/lib.rs @@ -11,6 +11,7 @@ )] pub mod contract; +mod did; mod error; pub mod msg; pub mod state;