From a0d055d4181997d54403c759b33216786958bcb9 Mon Sep 17 00:00:00 2001 From: Arnaud Mimart <33665250+amimart@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:02:51 +0100 Subject: [PATCH] feat(dataverse): add DataIntegrityProof vc support --- .../okp4-dataverse/src/credential/proof.rs | 228 +++++++++++++++--- .../src/credential/rdf_marker.rs | 3 + contracts/okp4-dataverse/src/credential/vc.rs | 4 +- 3 files changed, 194 insertions(+), 41 deletions(-) diff --git a/contracts/okp4-dataverse/src/credential/proof.rs b/contracts/okp4-dataverse/src/credential/proof.rs index 9259d0b4..11cc9019 100644 --- a/contracts/okp4-dataverse/src/credential/proof.rs +++ b/contracts/okp4-dataverse/src/credential/proof.rs @@ -1,8 +1,9 @@ use crate::credential::crypto::{CanonicalizationAlg, CryptoSuite, DigestAlg, SignatureAlg}; use crate::credential::error::InvalidProofError; use crate::credential::rdf_marker::{ - PROOF_RDF_PROOF_PURPOSE, PROOF_RDF_PROOF_VALUE, PROOF_RDF_PROOF_VALUE_TYPE, - PROOF_RDF_VERIFICATION_METHOD, RDF_CREATED, RDF_DATE_TYPE, RDF_TYPE, + PROOF_RDF_CRYPTOSUITE, PROOF_RDF_PROOF_PURPOSE, PROOF_RDF_PROOF_VALUE, + PROOF_RDF_PROOF_VALUE_TYPE, PROOF_RDF_VERIFICATION_METHOD, RDF_CREATED, RDF_DATE_TYPE, + RDF_TYPE, }; use itertools::Itertools; use okp4_rdf::dataset::{Dataset, QuadIterator}; @@ -11,15 +12,19 @@ use rio_api::model::{GraphName, Literal, Quad, Term}; #[derive(Debug, PartialEq)] pub enum Proof<'a> { Ed25519Signature2020(Ed25519Signature2020Proof<'a>), + DataIntegrity(DataIntegrityProof<'a>), } impl<'a> Proof<'a> { pub fn suitable(&self, issuer: &str, purpose: ProofPurpose) -> bool { - match self { + let (controller, proof_purpose) = match self { Self::Ed25519Signature2020(proof) => { - proof.verification_method.controller == issuer && proof.purpose == purpose + (proof.verification_method.controller, proof.purpose) } - } + Proof::DataIntegrity(proof) => (proof.verification_method.controller, proof.purpose), + }; + + controller == issuer && proof_purpose == purpose } pub fn crypto_suite(&self) -> CryptoSuite { @@ -28,26 +33,34 @@ impl<'a> Proof<'a> { CanonicalizationAlg::Urdna2015, DigestAlg::Sha256, SignatureAlg::Ed25519, - ) - .into(), + ), + Proof::DataIntegrity(p) => ( + CanonicalizationAlg::Urdna2015, + DigestAlg::Sha256, + p.cryptosuite.into(), + ), } + .into() } pub fn pub_key(&'a self) -> &'a [u8] { match self { Proof::Ed25519Signature2020(p) => &p.verification_method.pub_key, + Proof::DataIntegrity(p) => &p.verification_method.pub_key, } } pub fn signature(&'a self) -> &'a [u8] { match self { Proof::Ed25519Signature2020(p) => &p.value, + Proof::DataIntegrity(p) => &p.value, } } pub fn options(&'a self) -> &'a [Quad<'a>] { match self { Proof::Ed25519Signature2020(p) => p.options.as_ref(), + Proof::DataIntegrity(p) => p.options.as_ref(), } } @@ -78,6 +91,20 @@ impl<'a> Proof<'a> { }) } + fn parse_verification_method(raw: &'a str) -> Result<(&'a str, &'a str), InvalidProofError> { + Ok(match raw.split('#').collect::>()[..] { + [controller, key] => match controller.split(':').collect::>()[..] { + ["did", "key", controller_key] if controller_key == key => (controller, key), + _ => Err(InvalidProofError::Malformed( + "couldn't parse did key for verification method".to_string(), + ))?, + }, + _ => Err(InvalidProofError::Malformed( + "couldn't parse did key for verification method".to_string(), + ))?, + }) + } + fn extract_created( dataset: &'a Dataset<'a>, proof_graph: GraphName<'a>, @@ -159,6 +186,21 @@ impl<'a> Proof<'a> { )), }) } + + fn extract_proof_options(dataset: &'a Dataset<'a>, proof_graph: GraphName<'a>) -> Dataset<'a> { + Dataset::new( + dataset + .match_pattern(None, None, None, Some(Some(proof_graph))) + .skip_pattern((None, Some(PROOF_RDF_PROOF_VALUE), None, None).into()) + .map(|quad| Quad { + subject: quad.subject, + predicate: quad.predicate, + object: quad.object, + graph_name: None, + }) + .collect(), + ) + } } impl<'a> TryFrom<(&'a Dataset<'a>, GraphName<'a>)> for Proof<'a> { @@ -188,6 +230,9 @@ impl<'a> TryFrom<(&'a Dataset<'a>, GraphName<'a>)> for Proof<'a> { "https://w3id.org/security#Ed25519Signature2020" => Ok(Self::Ed25519Signature2020( Ed25519Signature2020Proof::try_from((dataset, proof_graph))?, )), + "https://w3id.org/security#DataIntegrityProof" => Ok(Self::DataIntegrity( + DataIntegrityProof::try_from((dataset, proof_graph))?, + )), _ => Err(InvalidProofError::Unsupported), } } @@ -202,6 +247,21 @@ pub struct Ed25519Signature2020Proof<'a> { options: Dataset<'a>, } +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum ProofPurpose { + AssertionMethod, + Unused, +} + +impl<'a> From<&'a str> for ProofPurpose { + fn from(value: &'a str) -> Self { + match value { + "https://w3id.org/security#assertionMethod" => ProofPurpose::AssertionMethod, + _ => ProofPurpose::Unused, + } + } +} + impl<'a> TryFrom<(&'a Dataset<'a>, GraphName<'a>)> for Ed25519Signature2020Proof<'a> { type Error = InvalidProofError; @@ -218,18 +278,7 @@ impl<'a> TryFrom<(&'a Dataset<'a>, GraphName<'a>)> for Ed25519Signature2020Proof created: Proof::extract_created(dataset, proof_graph)?, purpose: p_purpose.into(), value: p_value, - options: Dataset::new( - dataset - .match_pattern(None, None, None, Some(Some(proof_graph))) - .skip_pattern((None, Some(PROOF_RDF_PROOF_VALUE), None, None).into()) - .map(|quad| Quad { - subject: quad.subject, - predicate: quad.predicate, - object: quad.object, - graph_name: None, - }) - .collect(), - ), + options: Proof::extract_proof_options(dataset, proof_graph), }) } } @@ -245,39 +294,123 @@ impl<'a> TryFrom<&'a str> for Ed25519VerificationKey2020<'a> { type Error = InvalidProofError; fn try_from(value: &'a str) -> Result { - Ok(match value.split('#').collect::>()[..] { - [controller, key] => match controller.split(':').collect::>()[..] { - ["did", "key", controller_key] if controller_key == key => Self { - id: value, - controller, - pub_key: multiformats::decode_ed25519_key(key)?, - }, - _ => Err(InvalidProofError::Malformed( - "couldn't parse did key for verification method".to_string(), - ))?, - }, - _ => Err(InvalidProofError::Malformed( - "couldn't parse did key for verification method".to_string(), - ))?, + let (controller, key) = Proof::parse_verification_method(value)?; + Ok(Self { + id: value, + controller, + pub_key: multiformats::decode_ed25519_key(key)?, }) } } #[derive(Debug, PartialEq)] -pub enum ProofPurpose { - AssertionMethod, - Unused, +pub struct DataIntegrityProof<'a> { + cryptosuite: DataIntegrityCryptoSuite, + verification_method: Multikey<'a>, + created: &'a str, + purpose: ProofPurpose, + value: Vec, + options: Dataset<'a>, } -impl<'a> From<&'a str> for ProofPurpose { - fn from(value: &'a str) -> Self { +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum DataIntegrityCryptoSuite { + EddsaRdfc2022, +} + +impl From for SignatureAlg { + fn from(value: DataIntegrityCryptoSuite) -> Self { match value { - "https://w3id.org/security#assertionMethod" => ProofPurpose::AssertionMethod, - _ => ProofPurpose::Unused, + DataIntegrityCryptoSuite::EddsaRdfc2022 => SignatureAlg::Ed25519, } } } +impl<'a> DataIntegrityProof<'a> { + fn extract_cryptosuite( + dataset: &'a Dataset<'a>, + proof_graph: GraphName<'a>, + ) -> Result { + dataset + .match_pattern( + None, + Some(PROOF_RDF_CRYPTOSUITE), + None, + Some(Some(proof_graph)), + ) + .objects() + .exactly_one() + .map_err(|e| match e.size_hint() { + (_, Some(_)) => InvalidProofError::Malformed( + "Proof cannot have more than one proof cryptosuite".to_string(), + ), + _ => InvalidProofError::MissingProofCryptosuite, + }) + .and_then(|o| match o { + Term::Literal(Literal::Simple { value }) + | Term::Literal(Literal::Typed { value, datatype: _ }) => Ok(value), + _ => Err(InvalidProofError::Malformed( + "Proof cryptosuite must be a cryptosuite string".to_string(), + )), + }) + .and_then(|suite| { + Ok(match suite { + "eddsa-rdfc-2022" | "eddsa-2022" => DataIntegrityCryptoSuite::EddsaRdfc2022, + _ => Err(InvalidProofError::Malformed( + "Proof cryptosuite unknown or unsupported".to_string(), + ))?, + }) + }) + } +} + +impl<'a> TryFrom<(&'a Dataset<'a>, GraphName<'a>)> for DataIntegrityProof<'a> { + type Error = InvalidProofError; + + fn try_from( + (dataset, proof_graph): (&'a Dataset<'a>, GraphName<'a>), + ) -> Result { + let cryptosuite = DataIntegrityProof::extract_cryptosuite(dataset, proof_graph)?; + let v_method = Proof::extract_verification_method(dataset, proof_graph)?; + let p_purpose = Proof::extract_proof_purpose(dataset, proof_graph)?; + let (_, p_value) = multibase::decode(Proof::extract_proof_value(dataset, proof_graph)?) + .map_err(InvalidProofError::from)?; + + Ok(Self { + cryptosuite, + verification_method: (v_method, cryptosuite).try_into()?, + created: Proof::extract_created(dataset, proof_graph)?, + purpose: p_purpose.into(), + value: p_value, + options: Proof::extract_proof_options(dataset, proof_graph), + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct Multikey<'a> { + id: &'a str, + controller: &'a str, + pub_key: Vec, +} + +impl<'a> TryFrom<(&'a str, DataIntegrityCryptoSuite)> for Multikey<'a> { + type Error = InvalidProofError; + + fn try_from( + (value, cryptosuite): (&'a str, DataIntegrityCryptoSuite), + ) -> Result { + let (controller, key) = Proof::parse_verification_method(value)?; + Ok(Self { + id: value, + controller, + pub_key: match cryptosuite { + DataIntegrityCryptoSuite::EddsaRdfc2022 => multiformats::decode_ed25519_key(key), + }?, + }) + } +} + mod multiformats { use crate::credential::error::InvalidProofError; use multibase::Base; @@ -296,6 +429,21 @@ mod multiformats { Ok(key.to_vec()) } + + pub fn decode_secp256k1_key(src: &str) -> Result, InvalidProofError> { + let (base, data) = multibase::decode(src).map_err(|_| InvalidProofError::InvalidPubKey)?; + if base != Base::Base58Btc { + Err(InvalidProofError::InvalidPubKey)?; + } + + let (codec, key) = + unsigned_varint::decode::u16(&data).map_err(|_| InvalidProofError::InvalidPubKey)?; + if codec != 0xe7 { + Err(InvalidProofError::InvalidPubKey)?; + } + + Ok(key.to_vec()) + } } #[cfg(test)] diff --git a/contracts/okp4-dataverse/src/credential/rdf_marker.rs b/contracts/okp4-dataverse/src/credential/rdf_marker.rs index c772d1ef..346574e0 100644 --- a/contracts/okp4-dataverse/src/credential/rdf_marker.rs +++ b/contracts/okp4-dataverse/src/credential/rdf_marker.rs @@ -44,3 +44,6 @@ pub const PROOF_RDF_PROOF_VALUE: NamedNode<'_> = NamedNode { pub const PROOF_RDF_PROOF_VALUE_TYPE: NamedNode<'_> = NamedNode { iri: "https://w3id.org/security#multibase", }; +pub const PROOF_RDF_CRYPTOSUITE: NamedNode<'_> = NamedNode { + iri: "https://w3id.org/security#cryptosuite", +}; diff --git a/contracts/okp4-dataverse/src/credential/vc.rs b/contracts/okp4-dataverse/src/credential/vc.rs index 183de73d..06fab267 100644 --- a/contracts/okp4-dataverse/src/credential/vc.rs +++ b/contracts/okp4-dataverse/src/credential/vc.rs @@ -298,6 +298,7 @@ impl<'a> VerifiableCredential<'a> { mod test { use super::*; use crate::testutil::testutil; + use cosmwasm_std::testing::mock_dependencies; #[test] fn proper_vc_from_dataset() { @@ -335,10 +336,11 @@ mod test { #[test] fn vc_verify() { + let mut deps = mock_dependencies(); let owned_quads = testutil::read_test_quads("vc-ok.nq"); let dataset = Dataset::from(owned_quads.as_slice()); let vc = VerifiableCredential::try_from(&dataset).unwrap(); - let verif_res = vc.verify(); + let verif_res = vc.verify(deps.as_mut()); assert!(verif_res.is_ok()); } }