Skip to main content

trust_tasks_proof/
proof_ext.rs

1//! [`ProofExt`] — `.sign()` and `.verify()` as methods on a typed
2//! `TrustTask<P>`. Private module; the trait is re-exported at the crate
3//! root and carries the documentation.
4
5use async_trait::async_trait;
6use serde::Serialize;
7use trust_tasks_rs::{ProofVerifier, TrustTask, VerificationError};
8
9#[cfg(feature = "affinidi")]
10use crate::affinidi::{sign_trust_task, AffinidiSigner, SignError, SignOptions};
11#[cfg(feature = "affinidi")]
12use trust_tasks_rs::Proof;
13
14/// Extension trait adding [`sign`](Self::sign) and
15/// [`verify`](Self::verify) to the framework's [`TrustTask<P>`], for
16/// every payload type `P` a producer can serialise.
17///
18/// # Why this trait exists
19///
20/// [`sign_trust_task`](crate::affinidi::sign_trust_task) operates on a
21/// [`serde_json::Value`], because a W3C Data Integrity proof is computed
22/// over the *document's JSON form* and the framework's document type is
23/// generic over its payload. That is the right shape for the primitive
24/// and the wrong shape for a producer, who holds a `TrustTask<P>` and
25/// wants a signed `TrustTask<P>` back. Without this trait, signing is a
26/// five-step ritual — serialise, call, check, deserialise, reassign —
27/// that every producer writes out by hand and can get subtly wrong (most
28/// often by mutating the document *after* signing it).
29///
30/// `ProofExt` is a thin typed wrapper over the free functions, not a
31/// replacement for them. It reuses
32/// [`sign_trust_task`](crate::affinidi::sign_trust_task) verbatim, so the
33/// canonicalisation contract, the deterministic `eddsa-jcs-2022` default,
34/// the replace-don't-nest rule for an existing proof, and the SPEC.md
35/// §4.7/§4.8 issuer↔`verificationMethod` pre-flight are all exactly what
36/// that function already implements. A document signed through this trait
37/// and one signed through the free function are byte-identical.
38///
39/// ```rust,ignore
40/// use trust_tasks_proof::{affinidi::{SignOptions, Verifier}, ProofExt};
41/// use trust_tasks_rs::{specs::acl::grant::v0_1 as grant, TrustTask};
42///
43/// let mut req = TrustTask::for_payload(new_id(), grant::Payload { /* … */ });
44/// req.issuer = Some(my_did.clone());       // set every member first …
45/// req.recipient = Some(server_did.clone());
46/// req.sign(&secret, SignOptions::new()).await?;   // … then sign.
47///
48/// // Consumer side, same trait:
49/// req.verify(&Verifier::for_did_key()).await?;
50/// ```
51///
52/// # ⚠ Sign last
53///
54/// The proof covers the document as it stands at the moment
55/// [`sign`](Self::sign) is called. Mutating any member afterwards
56/// invalidates the signature, and nothing in the type system stops you —
57/// `sign` takes `&mut self` precisely so the call reads as the final step
58/// of composing the document. Re-signing after a change is always safe:
59/// the existing proof is discarded and a fresh one minted over the
60/// current content.
61#[async_trait]
62pub trait ProofExt {
63    /// Sign this document in place, attaching the resulting Data
64    /// Integrity proof to its `proof` member.
65    ///
66    /// Equivalent to serialising the document, calling
67    /// [`sign_trust_task`](crate::affinidi::sign_trust_task), and
68    /// deserialising the result — the round-trip through
69    /// [`serde_json::Value`] happens inside, over exactly the same
70    /// unsigned bytes, so the emitted proof is identical to the free
71    /// function's.
72    ///
73    /// Every rule the free function applies applies here:
74    ///
75    /// * The signature is computed over the document **minus** its
76    ///   `proof` member. An existing proof is replaced, never nested and
77    ///   never signed over.
78    /// * The document MUST already carry an in-band `issuer` equal to
79    ///   the DID of the signer's `verificationMethod` (the part before
80    ///   `#`). Otherwise the call fails with
81    ///   [`SignError::MissingIssuer`] / [`SignError::IssuerMismatch`]
82    ///   *before* a signature is produced, rather than emitting a
83    ///   document no conforming verifier could accept.
84    /// * `proofPurpose` defaults to `assertionMethod` and the
85    ///   cryptosuite to `eddsa-jcs-2022` unless
86    ///   [`SignOptions`](crate::affinidi::SignOptions) says otherwise.
87    ///
88    /// On error, `self` is left untouched — a failed sign never leaves a
89    /// half-signed document behind.
90    ///
91    /// Available with the `affinidi` feature (on by default).
92    #[cfg(feature = "affinidi")]
93    async fn sign(
94        &mut self,
95        signer: &dyn AffinidiSigner,
96        options: SignOptions,
97    ) -> Result<(), SignError>;
98
99    /// Verify this document's `proof` with `verifier`.
100    ///
101    /// The mirror of [`sign`](Self::sign), and the argument-order flip of
102    /// [`ProofVerifier::verify`] — the same check, spelled from the
103    /// document's point of view so a consumer holding a `TrustTask<P>`
104    /// does not have to reach for the verifier first.
105    ///
106    /// Returns [`VerificationError`] on every failure mode, including a
107    /// document that carries no `proof` at all (the framework's
108    /// `proofRequired` check is a separate, earlier concern — see
109    /// [`TrustTask::enforce_spec_policy`]).
110    async fn verify<V>(&self, verifier: &V) -> Result<(), VerificationError>
111    where
112        V: ProofVerifier + ?Sized;
113}
114
115#[async_trait]
116impl<P> ProofExt for TrustTask<P>
117where
118    P: Serialize + Send + Sync,
119{
120    #[cfg(feature = "affinidi")]
121    async fn sign(
122        &mut self,
123        signer: &dyn AffinidiSigner,
124        options: SignOptions,
125    ) -> Result<(), SignError> {
126        // The document as the verifier will see it. `sign_trust_task`
127        // strips any existing `proof` itself, so nothing is done to
128        // `self` before the signature exists — which is what makes the
129        // failure paths below non-destructive.
130        let unsigned = serde_json::to_value(&*self)?;
131        let signed = sign_trust_task(&unsigned, signer, options).await?;
132
133        // Lift only the `proof` member back onto the typed document.
134        // Every other member came from `self` unchanged, so re-parsing
135        // the whole document into `TrustTask<P>` would add a lossy
136        // deserialise step for no gain — and a payload type whose serde
137        // round-trip is not exact would silently invalidate the proof it
138        // was just given.
139        let proof = signed
140            .get("proof")
141            .cloned()
142            .ok_or_else(|| SignError::ProofRoundTrip("signed document carries no proof".into()))?;
143        let proof: Proof =
144            serde_json::from_value(proof).map_err(|e| SignError::ProofRoundTrip(e.to_string()))?;
145
146        self.proof = Some(proof);
147        Ok(())
148    }
149
150    async fn verify<V>(&self, verifier: &V) -> Result<(), VerificationError>
151    where
152        V: ProofVerifier + ?Sized,
153    {
154        verifier.verify(self).await
155    }
156}