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/// Default tolerance applied to a proof's `created` timestamp when it
94/// sits in the verifier's future: 60 seconds.
95///
96/// `created` is stamped by the *signer's* clock and checked against the
97/// *verifier's*. With zero tolerance, acceptance of an otherwise-valid
98/// proof becomes a race between clock skew and delivery latency — the
99/// same signed request is accepted or rejected depending on how quickly
100/// it arrives. 60s matches the leeway conventionally applied to JWT
101/// `iat`/`nbf` (and `jsonwebtoken`'s own default), so a signer skewed
102/// further than this generally fails bearer-token validation anyway.
103///
104/// This governs only how far *ahead* `created` may be. It is not a
105/// freshness window: this library does not reject old proofs, so replay
106/// protection must come from the surrounding protocol.
107pub const DEFAULT_CLOCK_SKEW: chrono::TimeDelta = chrono::TimeDelta::seconds(60);
108
109/// Options for verifying a Data Integrity proof.
110///
111/// Currently carries the document's externally-supplied `@context` (for
112/// comparison with the proof's declared context), an optional allowlist
113/// of acceptable cryptosuites, and the tolerance applied to a future
114/// `created` timestamp. More fields will be added as the library grows —
115/// `#[non_exhaustive]` ensures future additions do not break callers.
116#[derive(Clone, Debug)]
117#[non_exhaustive]
118pub struct VerifyOptions {
119 /// Expected `@context` of the signed document. When `Some`, the
120 /// verifier enforces that the proof's `@context` matches.
121 pub expected_context: Option<Vec<String>>,
122
123 /// If non-empty, the proof's `cryptosuite` must appear in this list.
124 /// Use to reject proofs produced by suites your policy does not
125 /// accept (e.g. refuse `bbs-2023` in a context that requires full
126 /// disclosure).
127 pub allowed_suites: Vec<CryptoSuite>,
128
129 /// How far into the verifier's future a proof's `created` may sit
130 /// before it is rejected as non-conformant. Defaults to
131 /// [`DEFAULT_CLOCK_SKEW`]; set to zero for strict rejection of any
132 /// future timestamp. Negative values are treated as zero.
133 pub clock_skew: chrono::TimeDelta,
134}
135
136impl Default for VerifyOptions {
137 /// All checks off except a [`DEFAULT_CLOCK_SKEW`] allowance on
138 /// `created` — deliberately *not* `#[derive]`d, which would mean zero
139 /// tolerance and make verification skew-sensitive by default.
140 fn default() -> Self {
141 Self {
142 expected_context: None,
143 allowed_suites: Vec::new(),
144 clock_skew: DEFAULT_CLOCK_SKEW,
145 }
146 }
147}
148
149impl VerifyOptions {
150 /// Constructs a `VerifyOptions` with no context or cryptosuite
151 /// restrictions and the default clock-skew allowance. Equivalent to
152 /// [`VerifyOptions::default`].
153 #[must_use = "constructed options must be passed to sign/verify to take effect"]
154 pub fn new() -> Self {
155 Self::default()
156 }
157
158 /// Sets the expected document `@context`.
159 #[must_use = "chained builder call returns self; assign or chain further"]
160 pub fn with_expected_context(mut self, ctx: Vec<String>) -> Self {
161 self.expected_context = Some(ctx);
162 self
163 }
164
165 /// Restricts the set of cryptosuites the verifier will accept.
166 #[must_use = "chained builder call returns self; assign or chain further"]
167 pub fn with_allowed_suites(mut self, suites: Vec<CryptoSuite>) -> Self {
168 self.allowed_suites = suites;
169 self
170 }
171
172 /// Overrides the tolerance for a `created` timestamp in the
173 /// verifier's future. Pass [`chrono::TimeDelta::zero`] to reject any
174 /// future timestamp outright.
175 #[must_use = "chained builder call returns self; assign or chain further"]
176 pub fn with_clock_skew(mut self, skew: chrono::TimeDelta) -> Self {
177 self.clock_skew = skew;
178 self
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn sign_options_builder_chains() {
188 let opts = SignOptions::new()
189 .with_context(vec!["https://example/ctx".into()])
190 .with_proof_purpose("authentication");
191 assert_eq!(
192 opts.context.as_deref(),
193 Some(&["https://example/ctx".to_string()][..])
194 );
195 assert_eq!(opts.proof_purpose.as_deref(), Some("authentication"));
196 assert!(opts.created.is_none());
197 }
198
199 #[test]
200 fn verify_options_builder_chains() {
201 let opts = VerifyOptions::new()
202 .with_expected_context(vec!["a".into()])
203 .with_allowed_suites(vec![]);
204 assert_eq!(
205 opts.expected_context.as_deref(),
206 Some(&["a".to_string()][..])
207 );
208 assert!(opts.allowed_suites.is_empty());
209 }
210}