ssi_zcap_ld/
lib.rs

1//! [ZCAP-LD][zcap-ld] implementation for SSI.
2//!
3//! [zcap-ld]: <https://w3c-ccg.github.io/zcap-spec/>
4pub mod error;
5use std::{borrow::Cow, collections::HashMap, hash::Hash};
6
7pub use error::Error;
8
9use iref::{Uri, UriBuf};
10use rdf_types::VocabularyMut;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use ssi_claims::{
14    chrono::{DateTime, Utc},
15    data_integrity::{
16        suite::{CryptographicSuiteSigning, InputProofOptions, InputSignatureOptions},
17        AnyDataIntegrity, AnyProofs, AnySignatureAlgorithm, AnySuite, CryptographicSuite,
18        DataIntegrity, Proof, Proofs,
19    },
20    vc::syntax::{Context, RequiredContext},
21    ClaimsValidity, DateTimeProvider, Eip712TypesLoaderProvider, InvalidClaims, ResolverProvider,
22    SignatureEnvironment, SignatureError, ValidateClaims, VerificationParameters,
23};
24use ssi_json_ld::{JsonLdError, JsonLdLoaderProvider, JsonLdNodeObject, JsonLdObject, Loader};
25use ssi_rdf::{Interpretation, LdEnvironment, LinkedDataResource, LinkedDataSubject};
26use ssi_verification_methods::{AnyMethod, ProofPurpose, VerificationMethodResolver};
27use ssi_verification_methods::{MessageSigner, Signer};
28use static_iref::iri;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct SecurityV2;
32
33impl RequiredContext for SecurityV2 {
34    const CONTEXT_IRI: &'static iref::Iri = iri!("https://w3id.org/security/v2");
35}
36
37#[derive(Debug, Serialize, Deserialize, Clone)]
38#[serde(rename_all = "camelCase")]
39pub struct DefaultProps<A> {
40    /// Capability action.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub capability_action: Option<A>,
43
44    /// Additional properties.
45    #[serde(flatten)]
46    pub extra_fields: HashMap<String, Value>,
47}
48
49impl<A> DefaultProps<A> {
50    pub fn new(capability_action: Option<A>) -> Self {
51        Self {
52            capability_action,
53            extra_fields: HashMap::new(),
54        }
55    }
56}
57
58/// ZCAP Delegation, generic over Caveat and
59/// additional properties
60#[derive(Debug, Serialize, Deserialize, Clone)]
61#[serde(rename_all = "camelCase")]
62pub struct Delegation<C, S = DefaultProps<String>> {
63    /// JSON-LD context.
64    #[serde(rename = "@context")]
65    pub context: Context<SecurityV2>,
66
67    /// Identifier.
68    pub id: UriBuf,
69
70    /// Parent capability.
71    pub parent_capability: UriBuf,
72
73    /// Invoker.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub invoker: Option<UriBuf>,
76
77    /// Caveat.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub caveat: Option<C>,
80
81    /// Additional properties.
82    #[serde(flatten)]
83    pub additional_properties: S,
84}
85
86impl<C, P> Delegation<C, P> {
87    /// Creates a new delegation.
88    pub fn new(id: UriBuf, parent_capability: UriBuf, additional_properties: P) -> Self {
89        Self {
90            context: Context::default(),
91            id,
92            parent_capability,
93            invoker: None,
94            caveat: None,
95            additional_properties,
96        }
97    }
98
99    pub fn validate(&self, proofs: &Proofs<AnySuite>) -> Result<(), DelegationValidationError> {
100        for proof in proofs.iter() {
101            if proof.configuration().proof_purpose != ProofPurpose::CapabilityDelegation {
102                return Err(DelegationValidationError::InvalidProofPurpose);
103            }
104        }
105
106        Ok(())
107    }
108
109    pub fn validate_invocation_proof(
110        &self,
111        proof: &Proof<AnySuite>,
112    ) -> Result<(), InvocationValidationError> {
113        let id: &Uri = proof
114            .extra_properties
115            .get("capability")
116            .and_then(json_syntax::Value::as_str)
117            .ok_or(InvocationValidationError::MissingTargetId)?
118            .try_into()
119            .map_err(|_| InvocationValidationError::IdMismatch)?;
120
121        if id != &self.id {
122            return Err(InvocationValidationError::IdMismatch);
123        };
124
125        if let Some(invoker) = &self.invoker {
126            if invoker.as_iri() != proof.configuration().verification_method.id() {
127                return Err(InvocationValidationError::IncorrectInvoker);
128            }
129        }
130
131        Ok(())
132    }
133
134    /// Sign the delegation.
135    pub async fn sign<S>(
136        self,
137        suite: AnySuite,
138        resolver: &impl VerificationMethodResolver<Method = AnyMethod>,
139        signer: S,
140        proof_configuration: InputProofOptions<AnySuite>,
141        capability_chain: &[&str],
142    ) -> Result<DataIntegrity<Self, AnySuite>, SignatureError>
143    where
144        C: Serialize,
145        P: Serialize,
146        S: Signer<AnyMethod>,
147        S::MessageSigner: MessageSigner<AnySignatureAlgorithm>,
148    {
149        self.sign_with(
150            suite,
151            SignatureEnvironment::default(),
152            resolver,
153            signer,
154            proof_configuration,
155            capability_chain,
156        )
157        .await
158    }
159
160    /// Sign the delegation with a custom cryptographic suite and environment.
161    pub async fn sign_with<D, E, R, S>(
162        self,
163        suite: D,
164        environment: E,
165        resolver: R,
166        signer: S,
167        mut proof_configuration: InputProofOptions<D>,
168        capability_chain: &[&str],
169    ) -> Result<DataIntegrity<Self, D>, SignatureError>
170    where
171        D: CryptographicSuiteSigning<Self, E, R, S>,
172        InputSignatureOptions<D>: Default,
173    {
174        proof_configuration.extra_properties.insert(
175            "capabilityChain".into(),
176            json_syntax::to_value(capability_chain).unwrap(),
177        );
178
179        if proof_configuration.proof_purpose != ProofPurpose::CapabilityDelegation {
180            // TODO invalid proof purpose.
181        }
182
183        suite
184            .sign_with(
185                environment,
186                self,
187                resolver,
188                signer,
189                proof_configuration,
190                Default::default(),
191            )
192            .await
193    }
194}
195
196pub trait TargetCapabilityProvider {
197    type Caveat;
198    type AdditionalProperties;
199
200    fn target_capability(&self) -> &Delegation<Self::Caveat, Self::AdditionalProperties>;
201}
202
203impl<E: TargetCapabilityProvider> TargetCapabilityProvider for &E {
204    type Caveat = E::Caveat;
205    type AdditionalProperties = E::AdditionalProperties;
206
207    fn target_capability(&self) -> &Delegation<Self::Caveat, Self::AdditionalProperties> {
208        E::target_capability(*self)
209    }
210}
211
212pub struct InvocationVerifier<'a, C, S, R, L1 = ssi_json_ld::ContextLoader, L2 = ()> {
213    pub resolver: R,
214    pub json_ld_loader: L1,
215    pub eip712_types_loader: L2,
216    pub date_time: Option<DateTime<Utc>>,
217    pub delegation: &'a Delegation<C, S>,
218}
219
220impl<'a, C, S, R> InvocationVerifier<'a, C, S, R> {
221    pub fn from_resolver(resolver: R, delegation: &'a Delegation<C, S>) -> Self {
222        Self::from_verifier(VerificationParameters::from_resolver(resolver), delegation)
223    }
224}
225
226impl<'a, R, L1, L2, C, S> InvocationVerifier<'a, C, S, R, L1, L2> {
227    pub fn from_verifier(
228        verifier: VerificationParameters<R, L1, L2>,
229        delegation: &'a Delegation<C, S>,
230    ) -> Self {
231        Self {
232            resolver: verifier.resolver,
233            json_ld_loader: verifier.json_ld_loader,
234            eip712_types_loader: verifier.eip712_types_loader,
235            date_time: verifier.date_time,
236            delegation,
237        }
238    }
239}
240
241impl<'v, 'a, R, L1, L2, C, S> InvocationVerifier<'a, C, S, &'v R, &'v L1, &'v L2> {
242    pub fn from_verifier_ref(
243        verifier: &'v VerificationParameters<R, L1, L2>,
244        delegation: &'a Delegation<C, S>,
245    ) -> Self {
246        Self {
247            resolver: &verifier.resolver,
248            json_ld_loader: &verifier.json_ld_loader,
249            eip712_types_loader: &verifier.eip712_types_loader,
250            date_time: verifier.date_time,
251            delegation,
252        }
253    }
254}
255
256impl<C, S, R, L1, L2> DateTimeProvider for InvocationVerifier<'_, C, S, R, L1, L2> {
257    fn date_time(&self) -> DateTime<Utc> {
258        self.date_time.unwrap_or_else(Utc::now)
259    }
260}
261
262impl<C, S, R, L1, L2> ResolverProvider for InvocationVerifier<'_, C, S, R, L1, L2> {
263    type Resolver = R;
264
265    fn resolver(&self) -> &Self::Resolver {
266        &self.resolver
267    }
268}
269
270impl<C, S, R, L1: ssi_json_ld::Loader, L2> JsonLdLoaderProvider
271    for InvocationVerifier<'_, C, S, R, L1, L2>
272{
273    type Loader = L1;
274
275    fn loader(&self) -> &Self::Loader {
276        &self.json_ld_loader
277    }
278}
279
280impl<C, S, R, L1, L2: ssi_eip712::TypesLoader> Eip712TypesLoaderProvider
281    for InvocationVerifier<'_, C, S, R, L1, L2>
282{
283    type Loader = L2;
284
285    fn eip712_types(&self) -> &Self::Loader {
286        &self.eip712_types_loader
287    }
288}
289
290impl<C, S, R, L1, L2> TargetCapabilityProvider for InvocationVerifier<'_, C, S, R, L1, L2> {
291    type Caveat = C;
292    type AdditionalProperties = S;
293
294    fn target_capability(&self) -> &Delegation<Self::Caveat, Self::AdditionalProperties> {
295        self.delegation
296    }
297}
298
299impl<C, P> JsonLdObject for Delegation<C, P> {
300    fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
301        Some(Cow::Borrowed(self.context.as_ref()))
302    }
303}
304
305impl<C, P> JsonLdNodeObject for Delegation<C, P> {}
306
307impl<C, S, E> ValidateClaims<E, AnyProofs> for Delegation<C, S> {
308    fn validate_claims(&self, _: &E, proofs: &AnyProofs) -> ClaimsValidity {
309        self.validate(proofs).map_err(InvalidClaims::other)
310    }
311}
312
313impl<C, P> ssi_json_ld::Expandable for Delegation<C, P>
314where
315    C: Serialize,
316    P: Serialize,
317{
318    type Error = JsonLdError;
319
320    type Expanded<I, V>
321        = ssi_json_ld::ExpandedDocument<V::Iri, V::BlankId>
322    where
323        I: Interpretation,
324        V: VocabularyMut,
325        V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
326        V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
327
328    async fn expand_with<I, V>(
329        &self,
330        ld: &mut LdEnvironment<V, I>,
331        loader: &impl Loader,
332    ) -> Result<Self::Expanded<I, V>, Self::Error>
333    where
334        I: Interpretation,
335        V: VocabularyMut,
336        V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
337        V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
338    {
339        let json = json_syntax::to_value(self).unwrap();
340        ssi_json_ld::CompactJsonLd(json)
341            .expand_with(ld, loader)
342            .await
343    }
344}
345
346#[derive(Debug, thiserror::Error)]
347pub enum DelegationValidationError {
348    #[error("invalid proof purpose")]
349    InvalidProofPurpose,
350}
351
352#[derive(Debug, thiserror::Error)]
353pub enum InvocationValidationError {
354    #[error("invalid proof purpose")]
355    InvalidProofPurpose,
356
357    #[error("Target Capability IDs don't match")]
358    IdMismatch,
359
360    #[error("Missing proof target capability ID")]
361    MissingTargetId,
362
363    #[error("Incorrect Invoker")]
364    IncorrectInvoker,
365}
366
367// limited initial definition of a ZCAP Invocation, generic over Action
368#[derive(Debug, Serialize, Deserialize, Clone)]
369#[serde(rename_all = "camelCase")]
370pub struct Invocation<P = DefaultProps<String>> {
371    /// JSON-LD context.
372    #[serde(rename = "@context")]
373    pub context: Context<SecurityV2>,
374
375    /// Identifier.
376    pub id: UriBuf,
377
378    /// Extra properties.
379    #[serde(flatten)]
380    pub property_set: P,
381}
382
383impl<P> Invocation<P> {
384    pub fn new(id: UriBuf, property_set: P) -> Self {
385        Self {
386            context: Context::default(),
387            id,
388            property_set,
389        }
390    }
391
392    /// Sign the delegation.
393    pub async fn sign<S>(
394        self,
395        suite: AnySuite,
396        resolver: impl VerificationMethodResolver<Method = AnyMethod>,
397        signer: S,
398        mut proof_configuration: InputProofOptions<AnySuite>,
399        target: &Uri,
400    ) -> Result<AnyDataIntegrity<Invocation<P>>, SignatureError>
401    where
402        P: Serialize,
403        S: Signer<AnyMethod>,
404        S::MessageSigner: MessageSigner<AnySignatureAlgorithm>,
405    {
406        proof_configuration
407            .extra_properties
408            .insert("capability".into(), json_syntax::to_value(target).unwrap());
409
410        suite
411            .sign(self, resolver, signer, proof_configuration)
412            .await
413    }
414
415    pub fn validate<C, Q>(
416        &self,
417        // TODO make this a list for delegation chains
418        target_capability: &Delegation<C, Q>,
419        proofs: &Proofs<AnySuite>,
420    ) -> Result<(), InvocationValidationError> {
421        for proof in proofs.iter() {
422            if proof.configuration().proof_purpose != ProofPurpose::CapabilityInvocation {
423                return Err(InvocationValidationError::InvalidProofPurpose);
424            }
425
426            target_capability.validate_invocation_proof(proof)?
427        }
428
429        Ok(())
430    }
431}
432
433impl<P> JsonLdObject for Invocation<P> {
434    fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
435        Some(Cow::Borrowed(self.context.as_ref()))
436    }
437}
438
439impl<P> JsonLdNodeObject for Invocation<P> {}
440
441impl<P> ssi_json_ld::Expandable for Invocation<P>
442where
443    P: Serialize,
444{
445    type Error = JsonLdError;
446
447    type Expanded<I, V>
448        = ssi_json_ld::ExpandedDocument<V::Iri, V::BlankId>
449    where
450        I: Interpretation,
451        V: VocabularyMut,
452        V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
453        V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
454
455    async fn expand_with<I, V>(
456        &self,
457        ld: &mut LdEnvironment<V, I>,
458        loader: &impl Loader,
459    ) -> Result<Self::Expanded<I, V>, Self::Error>
460    where
461        I: Interpretation,
462        V: VocabularyMut,
463        V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
464        V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
465    {
466        let json = json_syntax::to_value(self).unwrap();
467        ssi_json_ld::CompactJsonLd(json)
468            .expand_with(ld, loader)
469            .await
470    }
471}
472
473impl<E, S> ValidateClaims<E, AnyProofs> for Invocation<S>
474where
475    E: TargetCapabilityProvider,
476{
477    fn validate_claims(&self, env: &E, proofs: &AnyProofs) -> ClaimsValidity {
478        self.validate(env.target_capability(), proofs)
479            .map_err(InvalidClaims::other)
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use ssi_claims::VerificationParameters;
487    use ssi_data_integrity::DataIntegrity;
488    use ssi_dids_core::{example::ExampleDIDResolver, VerificationMethodDIDResolver};
489    use ssi_jwk::JWK;
490    use ssi_verification_methods::SingleSecretSigner;
491    use static_iref::uri;
492
493    #[derive(Deserialize, PartialEq, Debug, Clone, Serialize)]
494    enum Actions {
495        Read,
496        Write,
497    }
498
499    impl Default for Actions {
500        fn default() -> Self {
501            Self::Read
502        }
503    }
504
505    #[test]
506    fn delegation_from_json() {
507        let zcap_str = include_str!("../../../examples/files/zcap_delegation.jsonld");
508        let zcap: Delegation<(), ()> = serde_json::from_str(zcap_str).unwrap();
509        assert_eq!(
510            zcap.id,
511            uri!("https://whatacar.example/a-fancy-car/proc/7a397d7b")
512        );
513        assert_eq!(
514            zcap.parent_capability,
515            uri!("https://whatacar.example/a-fancy-car")
516        );
517        assert_eq!(
518            zcap.invoker.as_deref(),
519            Some(uri!("https://social.example/alyssa#key-for-car"))
520        );
521    }
522
523    #[test]
524    fn invocation_from_json() {
525        #[derive(Deserialize, PartialEq, Debug, Clone, Serialize)]
526        enum AC {
527            Drive,
528        }
529        let zcap_str = include_str!("../../../examples/files/zcap_invocation.jsonld");
530        let zcap: Invocation<DefaultProps<AC>> = serde_json::from_str(zcap_str).unwrap();
531        assert_eq!(
532            zcap.id,
533            uri!("urn:uuid:ad86cb2c-e9db-434a-beae-71b82120a8a4")
534        );
535        assert_eq!(zcap.property_set.capability_action, Some(AC::Drive));
536    }
537
538    #[async_std::test]
539    async fn round_trip() {
540        use ssi_data_integrity::ProofOptions;
541
542        let dk = VerificationMethodDIDResolver::new(ExampleDIDResolver::new());
543        let params = VerificationParameters::from_resolver(&dk);
544
545        let alice_did = "did:example:foo";
546        let alice_vm = UriBuf::new(format!("{}#key2", alice_did).into_bytes()).unwrap();
547        let alice = SingleSecretSigner::new(JWK {
548            key_id: Some(alice_vm.clone().into()),
549            ..serde_json::from_str(include_str!("../../../tests/ed25519-2020-10-18.json")).unwrap()
550        })
551        .into_local();
552
553        let bob_did = "did:example:bar";
554        let bob_vm = UriBuf::new(format!("{}#key1", bob_did).into_bytes()).unwrap();
555        let bob = SingleSecretSigner::new(JWK {
556            key_id: Some(bob_vm.clone().into()),
557            ..serde_json::from_str(include_str!("../../../tests/ed25519-2021-06-16.json")).unwrap()
558        })
559        .into_local();
560
561        let del: Delegation<(), DefaultProps<Actions>> = Delegation {
562            invoker: Some(bob_vm.clone()),
563            ..Delegation::new(
564                uri!("urn:a_urn").to_owned(),
565                uri!("kepler://alices_orbit").to_owned(),
566                DefaultProps::new(Some(Actions::Read)),
567            )
568        };
569        let inv: Invocation<DefaultProps<Actions>> = Invocation::new(
570            uri!("urn:a_different_urn").to_owned(),
571            DefaultProps::new(Some(Actions::Read)),
572        );
573
574        let ldpo_alice = ProofOptions::new(
575            "2024-02-13T16:25:26Z".parse().unwrap(),
576            alice_vm.clone().into_iri().into(),
577            ProofPurpose::CapabilityDelegation,
578            Default::default(),
579        );
580        let ldpo_bob = ProofOptions::new(
581            "2024-02-13T16:25:26Z".parse().unwrap(),
582            bob_vm.clone().into_iri().into(),
583            ProofPurpose::CapabilityInvocation,
584            Default::default(),
585        );
586
587        let signed_del = del
588            .clone()
589            .sign(
590                AnySuite::pick(alice.secret(), ldpo_alice.verification_method.as_ref()).unwrap(),
591                &dk,
592                &alice,
593                ldpo_alice.clone(),
594                &[],
595            )
596            .await
597            .unwrap();
598
599        let signed_inv = inv
600            .sign(
601                AnySuite::pick(bob.secret(), ldpo_bob.verification_method.as_ref()).unwrap(),
602                &dk,
603                &bob,
604                ldpo_bob,
605                &signed_del.id,
606            )
607            .await
608            .unwrap();
609
610        // happy path
611        assert!(signed_del.verify(&params).await.unwrap().is_ok());
612
613        assert!(signed_inv
614            .verify(InvocationVerifier::from_verifier_ref(
615                &params,
616                &signed_del.claims
617            ))
618            .await
619            .unwrap()
620            .is_ok());
621
622        let bad_sig_del = DataIntegrity::new(
623            Delegation {
624                invoker: Some(uri!("did:someone_else").to_owned()),
625                ..signed_del.claims.clone()
626            },
627            signed_del.proofs.clone(),
628        );
629
630        let mut bad_sig_inv = signed_inv.clone();
631        bad_sig_inv.id = uri!("urn:different_id").to_owned();
632
633        // invalid proof for data
634        assert!(bad_sig_del.verify(&params).await.unwrap().is_err());
635        assert!(bad_sig_inv
636            .verify(InvocationVerifier::from_verifier_ref(
637                &params,
638                &signed_del.claims
639            ))
640            .await
641            .unwrap()
642            .is_err());
643
644        // invalid cap attrs, invoker not matching
645        let wrong_del = Delegation {
646            invoker: Some(uri!("did:example:someone_else").to_owned()),
647            ..del.clone()
648        };
649        let signed_wrong_del = wrong_del
650            .sign(
651                AnySuite::pick(alice.secret(), ldpo_alice.verification_method.as_ref()).unwrap(),
652                &dk,
653                &alice,
654                ldpo_alice,
655                &[],
656            )
657            .await
658            .unwrap();
659        assert!(signed_inv
660            .verify(InvocationVerifier::from_verifier_ref(
661                &params,
662                &signed_wrong_del.claims
663            ))
664            .await
665            .unwrap()
666            .is_err());
667    }
668}