1use std::collections::HashMap as Map;
2use std::{convert::TryFrom, str::FromStr};
3
4use chrono::prelude::*;
5
6use crate::dataintegrity::DataIntegrityCryptoSuite;
7
8use super::*;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use ssi_dids::did_resolve::DIDResolver;
13use ssi_dids::VerificationRelationship as ProofPurpose;
14use ssi_json_ld::{json_to_dataset, parse_ld_context, ContextLoader};
15
16const RDF_TYPE: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
17const RDF_NIL: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil");
18const RDF_FIRST: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#first");
19const RDF_REST: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest");
20
21#[macro_export]
22macro_rules! assert_local {
23 ($cond:expr) => {
24 if !$cond {
25 return false;
26 }
27 };
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
31#[serde(rename_all = "camelCase")]
32pub struct Proof {
35 #[serde(rename = "@context")]
36 #[serde(default, skip_serializing_if = "Value::is_null")]
38 pub context: Value,
39 #[serde(rename = "type")]
40 pub type_: ProofSuiteType,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub proof_purpose: Option<ProofPurpose>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub proof_value: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub challenge: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub creator: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub verification_method: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub created: Option<DateTime<Utc>>, #[serde(skip_serializing_if = "Option::is_none")]
56 pub domain: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub nonce: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub jws: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub cryptosuite: Option<dataintegrity::DataIntegrityCryptoSuite>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 #[serde(flatten)]
65 pub property_set: Option<Map<String, Value>>,
66}
67
68impl Proof {
69 pub fn new(type_: ProofSuiteType) -> Self {
70 Self {
71 type_,
72 context: Value::default(),
73 proof_purpose: None,
74 proof_value: None,
75 challenge: None,
76 creator: None,
77 verification_method: None,
78 created: None,
79 domain: None,
80 nonce: None,
81 jws: None,
82 property_set: None,
83 cryptosuite: None,
84 }
85 }
86
87 pub fn with_options(self, options: &LinkedDataProofOptions) -> Self {
88 Self {
89 proof_purpose: options.proof_purpose.clone(),
90 verification_method: options
91 .verification_method
92 .clone()
93 .map(|uri| uri.to_string()),
94 domain: options.domain.clone(),
95 challenge: options.challenge.clone(),
96 created: Some(options.created.unwrap_or_else(now_ns)),
97 ..self
98 }
99 }
100
101 pub fn with_properties(self, properties: Option<Map<String, Value>>) -> Self {
102 Self {
103 property_set: properties,
104 ..self
105 }
106 }
107
108 #[allow(clippy::ptr_arg)]
110 pub fn matches_options(&self, options: &LinkedDataProofOptions) -> bool {
111 if let Some(ref verification_method) = options.verification_method {
112 assert_local!(
113 self.verification_method.as_ref() == Some(&verification_method.to_string())
114 );
115 }
116 if let Some(created) = self.created {
117 assert_local!(options.created.unwrap_or_else(now_ns) >= created);
118 } else {
119 return false;
120 }
121 if let Some(ref challenge) = options.challenge {
122 assert_local!(self.challenge.as_ref() == Some(challenge));
123 }
124 if let Some(ref domain) = options.domain {
125 assert_local!(self.domain.as_ref() == Some(domain));
126 }
127 if let Some(ref proof_purpose) = options.proof_purpose {
128 assert_local!(self.proof_purpose.as_ref() == Some(proof_purpose));
129 }
130 if let Some(ref type_) = options.type_ {
131 assert_local!(&self.type_ == type_);
132 }
133 true
134 }
135
136 pub fn matches_vms(&self, allowed_vms: &[String]) -> bool {
138 if let Some(vm) = self.verification_method.as_ref() {
139 assert_local!(allowed_vms.contains(vm));
140 }
141 true
142 }
143
144 #[allow(clippy::ptr_arg)]
148 pub fn matches(&self, options: &LinkedDataProofOptions, allowed_vms: &Vec<String>) -> bool {
149 self.matches_options(options) && self.matches_vms(allowed_vms)
150 }
151
152 pub async fn verify(
153 &self,
154 document: &(dyn LinkedDataDocument + Sync),
155 resolver: &dyn DIDResolver,
156 context_loader: &mut ContextLoader,
157 ) -> VerificationResult {
158 LinkedDataProofs::verify(self, document, resolver, context_loader)
159 .await
160 .into()
161 }
162}
163
164#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
165#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
166impl LinkedDataDocument for Proof {
167 fn get_contexts(&self) -> Result<Option<String>, Error> {
168 Ok(None)
169 }
170
171 async fn to_dataset_for_signing(
172 &self,
173 parent: Option<&(dyn LinkedDataDocument + Sync)>,
174 context_loader: &mut ContextLoader,
175 ) -> Result<DataSet, Error> {
176 let mut copy = self.clone();
177 copy.jws = None;
178 copy.proof_value = None;
179 let json = json_syntax::to_value_with(copy, Default::default).unwrap();
180 let dataset = json_to_dataset(
181 json,
182 context_loader,
183 parent
184 .map(LinkedDataDocument::get_contexts)
185 .transpose()?
186 .flatten()
187 .as_deref()
188 .map(parse_ld_context)
189 .transpose()?,
190 )
191 .await?;
192
193 verify_proof_consistency(self, &dataset)?;
194 Ok(dataset)
195 }
196
197 fn to_value(&self) -> Result<Value, Error> {
198 Ok(serde_json::to_value(self)?)
199 }
200}
201
202#[derive(Debug, Serialize, Deserialize, Clone)]
204#[serde(rename_all = "camelCase")]
205#[serde(deny_unknown_fields)]
206pub struct LinkedDataProofOptions {
209 #[serde(skip_serializing_if = "Option::is_none")]
210 #[serde(rename = "type")]
211 pub type_: Option<ProofSuiteType>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub verification_method: Option<URI>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub proof_purpose: Option<ProofPurpose>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub created: Option<DateTime<Utc>>,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub challenge: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub domain: Option<String>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub checks: Option<Vec<Check>>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 #[cfg(feature = "eip")]
235 pub eip712_domain: Option<crate::eip712::ProofInfo>,
236 #[cfg(not(feature = "eip"))]
237 pub eip712_domain: Option<()>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub cryptosuite: Option<DataIntegrityCryptoSuite>,
240}
241
242impl Default for LinkedDataProofOptions {
243 fn default() -> Self {
244 Self {
245 verification_method: None,
246 proof_purpose: Some(ProofPurpose::default()),
247 created: Some(crate::now_ns()),
248 challenge: None,
249 domain: None,
250 checks: Some(vec![Check::Proof]),
251 eip712_domain: None,
252 type_: None,
253 cryptosuite: None,
254 }
255 }
256}
257
258#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
259#[serde(try_from = "String")]
260#[serde(rename_all = "camelCase")]
261#[non_exhaustive]
262pub enum Check {
263 Proof,
264 #[serde(rename = "JWS")]
265 JWS,
266 Status,
267}
268
269impl FromStr for Check {
270 type Err = Error;
271 fn from_str(purpose: &str) -> Result<Self, Self::Err> {
272 match purpose {
273 "proof" => Ok(Self::Proof),
274 "JWS" => Ok(Self::JWS),
275 "credentialStatus" => Ok(Self::Status),
276 _ => Err(Error::UnsupportedCheck),
277 }
278 }
279}
280
281impl TryFrom<String> for Check {
282 type Error = Error;
283 fn try_from(purpose: String) -> Result<Self, Self::Error> {
284 Self::from_str(&purpose)
285 }
286}
287
288impl From<Check> for String {
289 fn from(check: Check) -> String {
290 match check {
291 Check::Proof => "proof".to_string(),
292 Check::JWS => "JWS".to_string(),
293 Check::Status => "credentialStatus".to_string(),
294 }
295 }
296}
297
298#[derive(thiserror::Error, Debug)]
299pub enum ProofInconsistency {
300 #[error(
302 "RDF statement object does not match value. Predicate: {0}. Expected: {1}. Actual: {2}"
303 )]
304 ObjectMismatch(IriBuf, String, String),
305
306 #[error("Missing RDF statement object. Predicate: {0}. Expected value: {1}")]
308 ExpectedObject(IriBuf, String),
309
310 #[error("Unexpected RDF statement object. Predicate: {0}. Value: {1}")]
312 UnexpectedObject(IriBuf, String),
313
314 #[error("List item mismatch. Value in RDF: {0}. Value in JSON: {1}")]
316 ListItemMismatch(String, String),
317
318 #[error("Invalid list")]
320 InvalidList,
321
322 #[error("Unexpected end of list")]
324 UnexpectedEndOfList,
325
326 #[error("Expected end of list")]
328 ExpectedEndOfList,
329
330 #[error("Missing type")]
332 MissingType,
333
334 #[error("Invalid type value")]
336 InvalidType,
337
338 #[error("Missing associated context: {0}")]
340 MissingAssociatedContext(IriBuf),
341
342 #[error("Unexpected triple {0:?}")]
344 UnexpectedTriple(Box<rdf_types::Triple>),
345}
346
347fn verify_proof_consistency(
349 proof: &Proof,
350 dataset: &DataSet,
351) -> Result<(), Box<ProofInconsistency>> {
352 let mut dataset = dataset.clone();
353 let graph_ref = dataset.default_graph_mut();
354
355 let type_triple = graph_ref
356 .take_match::<rdf_types::Subject, _, rdf_types::Object>(rdf_types::Triple(
357 None,
358 Some(&RDF_TYPE),
359 None,
360 ))
361 .ok_or(ProofInconsistency::MissingType)?;
362
363 let type_iri = type_triple
364 .object()
365 .as_iri()
366 .ok_or(ProofInconsistency::InvalidType)?;
367
368 if !proof
369 .type_
370 .associated_contexts()
371 .contains(&type_iri.as_str())
372 {
373 return Err(Box::new(ProofInconsistency::MissingAssociatedContext(
374 type_iri.clone(),
375 )));
376 }
377
378 let proof_id = type_triple.subject();
379
380 graph_ref.take_object_and_assert_eq_iri(
381 proof_id,
382 iri!("https://w3id.org/security#proofPurpose"),
383 proof.proof_purpose.as_ref().map(|pp| pp.to_iri()),
384 )?;
385
386 graph_ref.take_object_and_assert_eq_iri(
390 proof_id,
391 iri!("https://w3id.org/security#verificationMethod"),
392 proof
393 .verification_method
394 .as_ref()
395 .map(|m| Iri::new(m).unwrap()),
396 )?;
397
398 graph_ref.take_object_and_assert_eq_iri_or_str(
399 proof_id,
400 iri!("https://w3id.org/security#challenge"),
401 proof.challenge.as_deref(),
402 )?;
403
404 graph_ref.take_object_and_assert_eq_iri_or_str(
405 proof_id,
406 iri!("https://w3id.org/security#domain"),
407 proof.domain.as_deref(),
408 )?;
409
410 graph_ref.take_object_and_assert_eq_date(
411 proof_id,
412 iri!("http://purl.org/dc/terms/created"),
413 proof.created.as_ref(),
414 )?;
415
416 graph_ref.take_object_and_assert_eq_json(
417 proof_id,
418 iri!("https://w3id.org/security#publicKeyJwk"),
419 proof
420 .property_set
421 .as_ref()
422 .and_then(|cc| cc.get("publicKeyJwk")),
423 )?;
424
425 graph_ref.take_object_and_assert_eq_multibase(
426 proof_id,
427 iri!("https://w3id.org/security#publicKeyMultibase"),
428 proof
429 .property_set
430 .as_ref()
431 .and_then(|cc| cc.get("publicKeyMultibase"))
432 .and_then(|cap| cap.as_str()),
433 )?;
434
435 graph_ref.take_object_and_assert_eq_iri_or_str(
436 proof_id,
437 iri!("https://w3id.org/security#cryptosuite"),
438 proof
439 .cryptosuite
440 .clone()
441 .map(|cc| cc.to_string())
442 .as_deref(),
443 )?;
444
445 graph_ref.take_object_and_assert_eq_iri(
446 proof_id,
447 iri!("https://w3id.org/security#capability"),
448 proof
449 .property_set
450 .as_ref()
451 .and_then(|cc| cc.get("capability"))
452 .and_then(|cap| cap.as_str())
453 .map(|cap| Iri::new(cap).unwrap()),
454 )?;
455
456 graph_ref.take_object_and_assert_eq_list(
457 proof_id,
458 iri!("https://w3id.org/security#capabilityChain"),
459 proof
460 .property_set
461 .as_ref()
462 .and_then(|cc| cc.get("capabilityChain"))
463 .map(|cc| cc.as_array().unwrap().iter()),
464 |item, expected| match expected.as_str() {
465 Some(e) => match (item, Iri::new(e)) {
466 (rdf_types::Term::Iri(iri), Ok(expected)) => *iri == expected,
467 _ => false,
468 },
469 None => false,
470 },
471 )?;
472
473 if let Some(rdf_types::Triple(s, p, o)) = graph_ref.triples().next() {
475 return Err(Box::new(ProofInconsistency::UnexpectedTriple(Box::new(
476 rdf_types::Triple(s.clone(), p.clone(), o.clone()),
477 ))));
478 }
479
480 Ok(())
481}
482
483trait ProofGraph:
485 grdf::Graph<Subject = rdf_types::Subject, Predicate = IriBuf, Object = rdf_types::Object>
486 + for<'a> grdf::GraphTake<rdf_types::Subject, Iri<'a>, rdf_types::Object>
487{
488 fn take_object_and_assert<E>(
492 &mut self,
493 s: &Self::Subject,
494 p: Iri,
495 object_predicate: impl FnOnce(&mut Self, Option<Self::Object>) -> Result<(), E>,
496 ) -> Result<(), E> {
497 match self.take_match(rdf_types::Triple(Some(s), Some(&p), None)) {
498 Some(rdf_types::Triple(_, _, o)) => object_predicate(self, Some(o)),
499 None => object_predicate(self, None),
500 }
501 }
502
503 fn take_object_and_assert_eq<V: ToString>(
509 &mut self,
510 s: &Self::Subject,
511 p: Iri,
512 expected_o: Option<V>,
513 eq: impl FnOnce(&Self::Object, &V) -> bool,
514 ) -> Result<(), Box<ProofInconsistency>> {
515 self.take_object_and_assert(s, p, |_, o| match (o, expected_o) {
516 (Some(o), Some(expected)) => {
517 if eq(&o, &expected) {
518 Ok(())
519 } else {
520 Err(Box::new(ProofInconsistency::ObjectMismatch(
521 p.to_owned(),
522 expected.to_string(),
523 o.to_string(),
524 )))
525 }
526 }
527 (None, None) => Ok(()),
528 (None, Some(expected_iri)) => Err(Box::new(ProofInconsistency::ExpectedObject(
529 p.to_owned(),
530 expected_iri.to_string(),
531 ))),
532 (Some(o), None) => Err(Box::new(ProofInconsistency::UnexpectedObject(
533 p.to_owned(),
534 o.to_string(),
535 ))),
536 })
537 }
538
539 fn take_object_and_assert_eq_list<I: Iterator>(
545 &mut self,
546 s: &Self::Subject,
547 p: Iri,
548 expected_o: Option<I>,
549 eq: impl Fn(&Self::Object, &I::Item) -> bool,
550 ) -> Result<(), Box<ProofInconsistency>>
551 where
552 I::Item: ToString,
553 {
554 fn format_list<I: Iterator>(list: I) -> String
555 where
556 I::Item: ToString,
557 {
558 let mut expected_string = "[".to_string();
559
560 for (i, item) in list.enumerate() {
561 if i > 0 {
562 expected_string.push(',');
563 }
564
565 expected_string.push_str(&item.to_string());
566 }
567
568 expected_string.push(']');
569 expected_string
570 }
571
572 self.take_object_and_assert(s, p, |this, o| match (o, expected_o) {
573 (Some(o), Some(expected)) => {
574 let mut head = match o {
575 rdf_types::Term::Iri(i) if i == RDF_NIL => None,
576 rdf_types::Term::Iri(i) => Some(rdf_types::Subject::Iri(i)),
577 rdf_types::Term::Blank(b) => Some(rdf_types::Subject::Blank(b)),
578 rdf_types::Term::Literal(l) => {
579 return Err(Box::new(ProofInconsistency::ObjectMismatch(
580 p.to_owned(),
581 l.to_string(),
582 format_list(expected),
583 )))
584 }
585 };
586
587 for expected_item in expected {
588 match head.take() {
589 Some(id) => {
590 match this.take_match(rdf_types::Triple(
591 Some(&id),
592 Some(&RDF_FIRST),
593 None,
594 )) {
595 Some(rdf_types::Triple(_, _, first)) => {
596 if !eq(&first, &expected_item) {
597 return Err(Box::new(
598 ProofInconsistency::ListItemMismatch(
599 first.to_string(),
600 expected_item.to_string(),
601 ),
602 ));
603 }
604
605 match this.take_match(rdf_types::Triple(
606 Some(&id),
607 Some(&RDF_REST),
608 None,
609 )) {
610 Some(rdf_types::Triple(_, _, rest)) => {
611 head = match rest {
612 rdf_types::Term::Iri(i) if i == RDF_NIL => None,
613 rdf_types::Term::Iri(i) => {
614 Some(rdf_types::Subject::Iri(i))
615 }
616 rdf_types::Term::Blank(b) => {
617 Some(rdf_types::Subject::Blank(b))
618 }
619 rdf_types::Term::Literal(_) => {
620 return Err(Box::new(
621 ProofInconsistency::InvalidList,
622 ))
623 }
624 };
625 }
626 None => {
627 return Err(Box::new(ProofInconsistency::InvalidList))
628 }
629 }
630 }
631 None => return Err(Box::new(ProofInconsistency::InvalidList)),
632 }
633 }
634 None => return Err(Box::new(ProofInconsistency::UnexpectedEndOfList)),
635 }
636 }
637
638 if head.is_some() {
639 return Err(Box::new(ProofInconsistency::ExpectedEndOfList));
640 }
641
642 Ok(())
643 }
644 (None, None) => Ok(()),
645 (None, Some(expected)) => Err(Box::new(ProofInconsistency::ExpectedObject(
646 p.to_owned(),
647 format_list(expected),
648 ))),
649 (Some(o), None) => Err(Box::new(ProofInconsistency::UnexpectedObject(
650 p.to_owned(),
651 o.to_string(),
652 ))),
653 })
654 }
655
656 fn take_object_and_assert_eq_iri(
662 &mut self,
663 s: &Self::Subject,
664 p: Iri,
665 expected_o: Option<Iri>,
666 ) -> Result<(), Box<ProofInconsistency>>;
667
668 fn take_object_and_assert_eq_iri_or_str(
674 &mut self,
675 s: &Self::Subject,
676 p: Iri,
677 expected_o: Option<&str>,
678 ) -> Result<(), Box<ProofInconsistency>>;
679
680 fn take_object_and_assert_eq_date(
686 &mut self,
687 s: &Self::Subject,
688 p: Iri,
689 expected_o: Option<&DateTime<Utc>>,
690 ) -> Result<(), Box<ProofInconsistency>>;
691
692 fn take_object_and_assert_eq_multibase(
698 &mut self,
699 s: &Self::Subject,
700 p: Iri,
701 expected_o: Option<&str>,
702 ) -> Result<(), Box<ProofInconsistency>>;
703
704 fn take_object_and_assert_eq_json(
710 &mut self,
711 s: &Self::Subject,
712 p: Iri,
713 expected_o: Option<&serde_json::Value>,
714 ) -> Result<(), Box<ProofInconsistency>>;
715}
716
717impl ProofGraph for grdf::HashGraph<rdf_types::Subject, IriBuf, rdf_types::Object> {
718 fn take_object_and_assert_eq_iri(
719 &mut self,
720 s: &Self::Subject,
721 p: Iri,
722 expected_o: Option<Iri>,
723 ) -> Result<(), Box<ProofInconsistency>> {
724 self.take_object_and_assert_eq(s, p, expected_o, |o, expected_iri| match o {
725 rdf_types::Object::Iri(iri) => iri == expected_iri,
726 _ => false,
727 })
728 }
729
730 fn take_object_and_assert_eq_iri_or_str(
731 &mut self,
732 s: &Self::Subject,
733 p: Iri,
734 expected_o: Option<&str>,
735 ) -> Result<(), Box<ProofInconsistency>> {
736 self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
737 rdf_types::Object::Iri(iri) => iri.as_str() == *expected,
738 rdf_types::Object::Literal(rdf_types::Literal::String(s)) => s.as_str() == *expected,
739 _ => false,
740 })
741 }
742
743 fn take_object_and_assert_eq_date(
744 &mut self,
745 s: &Self::Subject,
746 p: Iri,
747 expected_o: Option<&DateTime<Utc>>,
748 ) -> Result<(), Box<ProofInconsistency>> {
749 self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
750 rdf_types::Object::Literal(rdf_types::Literal::TypedString(date, ty))
751 if *ty == iri!("http://www.w3.org/2001/XMLSchema#dateTime") =>
752 {
753 match DateTime::parse_from_rfc3339(date.as_str()) {
754 Ok(date) => date == **expected,
755 _ => false,
756 }
757 }
758 _ => false,
759 })
760 }
761
762 fn take_object_and_assert_eq_multibase(
763 &mut self,
764 s: &Self::Subject,
765 p: Iri,
766 expected_o: Option<&str>,
767 ) -> Result<(), Box<ProofInconsistency>> {
768 self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
769 rdf_types::Object::Literal(rdf_types::Literal::TypedString(s, ty)) => {
770 *ty == iri!("https://w3id.org/security#multibase") && s.as_str() == *expected
771 }
772 _ => false,
773 })
774 }
775
776 fn take_object_and_assert_eq_json(
777 &mut self,
778 s: &Self::Subject,
779 p: Iri,
780 expected_o: Option<&serde_json::Value>,
781 ) -> Result<(), Box<ProofInconsistency>> {
782 self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
783 rdf_types::Object::Literal(rdf_types::Literal::TypedString(json, ty))
784 if *ty == iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON") =>
785 {
786 match serde_json::from_str::<serde_json::Value>(json) {
787 Ok(json) => json == **expected,
788 _ => false,
789 }
790 }
791 _ => false,
792 })
793 }
794}