Skip to content

Commit

Permalink
feat(dataverse): implements VC rdf parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
amimart committed Feb 5, 2024
1 parent 1c6c456 commit a972284
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 0 deletions.
2 changes: 2 additions & 0 deletions contracts/okp4-dataverse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions contracts/okp4-dataverse/src/did/consts.rs
Original file line number Diff line number Diff line change
@@ -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",
};
3 changes: 3 additions & 0 deletions contracts/okp4-dataverse/src/did/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod consts;
mod crypto;
mod vc;
262 changes: 262 additions & 0 deletions contracts/okp4-dataverse/src/did/vc.rs
Original file line number Diff line number Diff line change
@@ -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<Claim<'a>>,
status: Option<Status<'a>>,
proof: Vec<Proof<'a>>,
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<Self, Self::Error> {
let id = Self::extract_identifier(&dataset)?;

let mut proofs = vec![];
let mut unsecured_filter: Vec<QuadPattern<'_>> = 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<NamedNode<'a>, 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<Vec<&'a str>, 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<NamedNode<'a>, 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<Option<&'a str>, 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<Vec<Claim<'a>>, 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<Option<Status<'a>>, 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<Vec<(Proof<'a>, 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()
}
}
3 changes: 3 additions & 0 deletions contracts/okp4-dataverse/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ pub enum ContractError {

#[error("{0}")]
Instantiate2Address(#[from] Instantiate2AddressError),

#[error("Invalid credential: '{0}'")]
InvalidCredential(String),
}
1 change: 1 addition & 0 deletions contracts/okp4-dataverse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)]

pub mod contract;
mod did;
mod error;
pub mod msg;
pub mod state;
Expand Down

0 comments on commit a972284

Please sign in to comment.