Skip to main content

affinidi_data_integrity/
options.rs

1//! Options passed to [`crate::DataIntegrityProof::sign`] and
2//! [`crate::DataIntegrityProof::verify_with_public_key`].
3//!
4//! Both types are plain value structs with a hand-rolled `with_*` builder.
5//! No procedural macros, no extra dependencies. The builder style was
6//! chosen over `typed-builder` / `bon` to keep the public-facing
7//! production-grade dependency footprint minimal.
8//!
9//! # Example
10//!
11//! ```ignore
12//! use affinidi_data_integrity::{SignOptions, crypto_suites::CryptoSuite};
13//!
14//! let opts = SignOptions::new()
15//!     .with_context(vec!["https://www.w3.org/ns/credentials/v2".into()])
16//!     .with_cryptosuite(CryptoSuite::MlDsa44Jcs2024)
17//!     .with_proof_purpose("authentication");
18//! ```
19
20use chrono::{DateTime, Utc};
21
22use crate::crypto_suites::CryptoSuite;
23
24/// Options for signing a Data Integrity proof.
25///
26/// Construct via [`SignOptions::new`] (or [`SignOptions::default`]) then
27/// chain `with_*` methods. All fields default to `None` / empty; the
28/// library fills in spec-compliant defaults (current time for `created`,
29/// `"assertionMethod"` for `proof_purpose`, the signer's declared
30/// cryptosuite) where they are not overridden here.
31///
32/// `SignOptions` is `#[non_exhaustive]` from the outside: construct it
33/// only via the provided methods, not struct-literal syntax.
34#[derive(Clone, Debug, Default)]
35#[non_exhaustive]
36pub struct SignOptions {
37    /// JSON-LD `@context` values to place on the proof. If `None`, the
38    /// document's own `@context` is used (for RDFC canonicalization) or no
39    /// context is emitted (for JCS).
40    pub context: Option<Vec<String>>,
41
42    /// Proof creation timestamp. If `None`, `Utc::now()` is used.
43    pub created: Option<DateTime<Utc>>,
44
45    /// Overrides the signer's declared cryptosuite. If `None`, the
46    /// library uses `signer.cryptosuite()`.
47    pub cryptosuite: Option<CryptoSuite>,
48
49    /// Value of `proofPurpose`. Defaults to `"assertionMethod"`.
50    pub proof_purpose: Option<String>,
51}
52
53impl SignOptions {
54    /// Constructs an empty `SignOptions`. Equivalent to
55    /// [`SignOptions::default`].
56    #[must_use = "constructed options must be passed to sign/verify to take effect"]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Sets the `@context` value placed on the emitted proof.
62    #[must_use = "chained builder call returns self; assign or chain further"]
63    pub fn with_context(mut self, context: Vec<String>) -> Self {
64        self.context = Some(context);
65        self
66    }
67
68    /// Sets the `created` timestamp. Takes a typed `DateTime<Utc>`; the
69    /// library serialises it to ISO-8601 (seconds precision, `Z`-suffix)
70    /// at the serde boundary.
71    #[must_use = "chained builder call returns self; assign or chain further"]
72    pub fn with_created(mut self, created: DateTime<Utc>) -> Self {
73        self.created = Some(created);
74        self
75    }
76
77    /// Overrides the cryptosuite that would otherwise be chosen by the
78    /// signer's default ([`crate::signer::Signer::cryptosuite`]).
79    #[must_use = "chained builder call returns self; assign or chain further"]
80    pub fn with_cryptosuite(mut self, suite: CryptoSuite) -> Self {
81        self.cryptosuite = Some(suite);
82        self
83    }
84
85    /// Overrides `proofPurpose`. The default is `"assertionMethod"`.
86    #[must_use = "chained builder call returns self; assign or chain further"]
87    pub fn with_proof_purpose(mut self, purpose: impl Into<String>) -> Self {
88        self.proof_purpose = Some(purpose.into());
89        self
90    }
91}
92
93/// Options for verifying a Data Integrity proof.
94///
95/// Currently carries the document's externally-supplied `@context` (for
96/// comparison with the proof's declared context) and an optional allowlist
97/// of acceptable cryptosuites. More fields will be added as the library
98/// grows — `#[non_exhaustive]` ensures future additions do not break
99/// callers.
100#[derive(Clone, Debug, Default)]
101#[non_exhaustive]
102pub struct VerifyOptions {
103    /// Expected `@context` of the signed document. When `Some`, the
104    /// verifier enforces that the proof's `@context` matches.
105    pub expected_context: Option<Vec<String>>,
106
107    /// If non-empty, the proof's `cryptosuite` must appear in this list.
108    /// Use to reject proofs produced by suites your policy does not
109    /// accept (e.g. refuse `bbs-2023` in a context that requires full
110    /// disclosure).
111    pub allowed_suites: Vec<CryptoSuite>,
112}
113
114impl VerifyOptions {
115    /// Constructs an empty `VerifyOptions`. Equivalent to
116    /// [`VerifyOptions::default`].
117    #[must_use = "constructed options must be passed to sign/verify to take effect"]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Sets the expected document `@context`.
123    #[must_use = "chained builder call returns self; assign or chain further"]
124    pub fn with_expected_context(mut self, ctx: Vec<String>) -> Self {
125        self.expected_context = Some(ctx);
126        self
127    }
128
129    /// Restricts the set of cryptosuites the verifier will accept.
130    #[must_use = "chained builder call returns self; assign or chain further"]
131    pub fn with_allowed_suites(mut self, suites: Vec<CryptoSuite>) -> Self {
132        self.allowed_suites = suites;
133        self
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn sign_options_builder_chains() {
143        let opts = SignOptions::new()
144            .with_context(vec!["https://example/ctx".into()])
145            .with_proof_purpose("authentication");
146        assert_eq!(
147            opts.context.as_deref(),
148            Some(&["https://example/ctx".to_string()][..])
149        );
150        assert_eq!(opts.proof_purpose.as_deref(), Some("authentication"));
151        assert!(opts.created.is_none());
152    }
153
154    #[test]
155    fn verify_options_builder_chains() {
156        let opts = VerifyOptions::new()
157            .with_expected_context(vec!["a".into()])
158            .with_allowed_suites(vec![]);
159        assert_eq!(
160            opts.expected_context.as_deref(),
161            Some(&["a".to_string()][..])
162        );
163        assert!(opts.allowed_suites.is_empty());
164    }
165}