didwebvh-rs 0.5.0

Implementation of the did:webvh method in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
/*!
*   Handling of witnessing changes to the log entries
*/

use affinidi_data_integrity::crypto_suites::CryptoSuite;

use crate::{DIDWebVHError, Multibase};
use serde::{Deserialize, Serialize};
use std::fmt::Display;

pub mod proofs;
pub mod validate;

/// Runtime options for verifying witness proofs.
///
/// didwebvh 1.0 §"The Witness Proofs File" mandates that witness Data
/// Integrity proofs use the `eddsa-jcs-2022` cryptosuite with
/// `proofPurpose` of `assertionMethod`. `WitnessVerifyOptions` lets a
/// caller additively widen the accepted cryptosuite set (for example, to
/// interop with a non-spec implementation that emits PQC-signed witness
/// proofs) without forcing a feature-flag rebuild.
///
/// `#[non_exhaustive]` so future fields (e.g. expiry windows, allowed
/// `proofPurpose` values) don't break callers.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct WitnessVerifyOptions {
    /// Cryptosuites accepted for witness proofs in addition to the spec
    /// default `eddsa-jcs-2022`. Empty by default — strict spec mode.
    pub extra_allowed_suites: Vec<CryptoSuite>,
}

impl WitnessVerifyOptions {
    /// Strict spec-compliant defaults (only `eddsa-jcs-2022`).
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a cryptosuite to the accepted list.
    #[must_use]
    pub fn with_extra_allowed_suite(mut self, suite: CryptoSuite) -> Self {
        self.extra_allowed_suites.push(suite);
        self
    }

    /// Returns true if the supplied suite is accepted under these options.
    ///
    /// Checks the spec-mandated `eddsa-jcs-2022` first, then any additional
    /// suites the caller opted into.
    pub fn suite_is_allowed(&self, suite: CryptoSuite) -> bool {
        suite == CryptoSuite::EddsaJcs2022 || self.extra_allowed_suites.contains(&suite)
    }

    /// Enforce the structural constraints from didwebvh 1.0 §"The Witness
    /// Proofs File" on a witness [`affinidi_data_integrity::DataIntegrityProof`]: the cryptosuite
    /// must be accepted (either the spec default `eddsa-jcs-2022` or a
    /// caller-opted-in extra) and `proofPurpose` must be `assertionMethod`.
    ///
    /// Cryptographic signature verification is intentionally NOT performed
    /// here — this is the cheap pre-check that runs before signature
    /// verification inside [`crate::log_entry::LogEntry::validate_witness_proof`].
    /// Expose it publicly so a caller doing its own witness plumbing can
    /// enforce the same shape constraints without going through a full
    /// `validate()` pass.
    pub fn check_proof_shape(
        &self,
        proof: &affinidi_data_integrity::DataIntegrityProof,
    ) -> Result<(), DIDWebVHError> {
        crate::log_entry::enforce_witness_proof_shape(proof, self)
    }
}

/// Witness nodes
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Witnesses {
    /// Active witness configuration with a threshold and list of witness nodes.
    Value {
        /// Minimum number of witness proofs required for acceptance.
        threshold: u32,
        /// List of configured witness nodes.
        witnesses: Vec<Witness>,
    },
    // WARN: This must always go last, otherwise it will become the default as it matches on
    // anything
    /// No witnesses are configured.
    Empty {},
}

impl Witnesses {
    /// Create a new [`WitnessesBuilder`] for constructing a [`Witnesses`] value.
    pub fn builder() -> WitnessesBuilder {
        WitnessesBuilder {
            threshold: 1,
            witnesses: Vec::new(),
        }
    }

    /// Are any witnesses configured?
    pub fn is_empty(&self) -> bool {
        match self {
            Witnesses::Empty {} => true,
            Witnesses::Value { witnesses, .. } => witnesses.is_empty(),
        }
    }

    /// Checks Witnesses parameters for errors
    pub fn validate(&self) -> Result<(), DIDWebVHError> {
        if self.is_empty() {
            return Err(DIDWebVHError::ValidationError(
                "Witnesses are enabled, but no witness nodes are specified! Can not be empty!"
                    .to_string(),
            ));
        }

        match self {
            Witnesses::Value {
                threshold,
                witnesses,
            } => {
                if threshold < &1 {
                    return Err(DIDWebVHError::ValidationError(
                        "Witness threshold must be 1 or more".to_string(),
                    ));
                } else if witnesses.len() < *threshold as usize {
                    return Err(DIDWebVHError::ValidationError(format!(
                        "Number of Witnesses ({}) is less than the threshold ({})",
                        witnesses.len(),
                        threshold
                    )));
                }
            }
            _ => {
                return Err(DIDWebVHError::ValidationError(
                    "Empty Witness Parameter config found, but it wasn't detected. INTERNAL ERROR STATE"
                        .to_string(),
                ));
            }
        }
        Ok(())
    }

    /// Returns witnesses if they exist
    pub fn witnesses(&self) -> Option<&[Witness]> {
        match self {
            Witnesses::Empty {} => None,
            Witnesses::Value { witnesses, .. } => Some(witnesses),
        }
    }

    /// Returns threshold if it exists
    pub fn threshold(&self) -> Option<u32> {
        match self {
            Witnesses::Empty {} => None,
            Witnesses::Value { threshold, .. } => Some(*threshold),
        }
    }
}

/// Single Witness Node
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Witness {
    /// Multibase-encoded public key identifying this witness node.
    pub id: Multibase,
}

impl Display for Witness {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.id)
    }
}

impl Witness {
    /// Returns the witness ID as a `did:key:` DID.
    ///
    /// Handles both formats: if the stored ID already starts with `did:key:`,
    /// it is returned as-is; otherwise the prefix is prepended.
    pub fn as_did(&self) -> String {
        let id = self.id.as_str();
        if id.starts_with("did:key:") {
            id.to_string()
        } else {
            ["did:key:", id].concat()
        }
    }

    /// Returns the witness ID as a `did:key:z6...#z6...` verification method reference.
    ///
    /// The fragment is the raw multibase key (without the `did:key:` prefix).
    pub fn as_did_key(&self) -> String {
        let did = self.as_did();
        let raw_key = did.strip_prefix("did:key:").unwrap_or(did.as_str());
        [&did, "#", raw_key].concat()
    }
}

/// Builder for constructing a [`Witnesses`] configuration.
///
/// Defaults to a threshold of 1. Use [`build()`](Self::build) to validate
/// and produce the final [`Witnesses`] value.
pub struct WitnessesBuilder {
    threshold: u32,
    witnesses: Vec<Witness>,
}

impl WitnessesBuilder {
    /// Set the minimum number of witness proofs required.
    pub fn threshold(mut self, t: u32) -> Self {
        self.threshold = t;
        self
    }

    /// Add a single witness by its multibase-encoded public key.
    pub fn witness(mut self, id: Multibase) -> Self {
        self.witnesses.push(Witness { id });
        self
    }

    /// Add multiple witnesses from an iterator of multibase-encoded public keys.
    pub fn witnesses(mut self, ids: impl IntoIterator<Item = Multibase>) -> Self {
        self.witnesses
            .extend(ids.into_iter().map(|id| Witness { id }));
        self
    }

    /// Build and validate the [`Witnesses`] configuration.
    ///
    /// Returns an error if the threshold is zero or exceeds the number of witnesses.
    pub fn build(self) -> Result<Witnesses, DIDWebVHError> {
        let w = Witnesses::Value {
            threshold: self.threshold,
            witnesses: self.witnesses,
        };
        w.validate()?;
        Ok(w)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Multibase;

    /// Tests that validating an empty `Witnesses::Empty` variant returns an error.
    ///
    /// When witness configuration is explicitly enabled but contains no witness nodes,
    /// validation must fail. This prevents a DID WebVH log from claiming witness support
    /// while having no actual witnesses to attest to log entry changes.
    #[test]
    fn test_validate_empty_error() {
        let w = Witnesses::Empty {};
        assert!(w.validate().is_err());
    }

    /// Tests that a threshold of zero is rejected during validation.
    ///
    /// Even when witness nodes are present, a threshold of zero would mean no witness
    /// signatures are required, effectively bypassing the witness mechanism entirely.
    /// DID WebVH requires a threshold of at least 1 to ensure that witness attestation
    /// is meaningful and provides actual security guarantees.
    #[test]
    fn test_validate_threshold_zero_error() {
        let w = Witnesses::Value {
            threshold: 0,
            witnesses: vec![Witness {
                id: Multibase::new("w1"),
            }],
        };
        assert!(w.validate().is_err());
    }

    /// Tests that validation fails when the threshold exceeds the number of witnesses.
    ///
    /// If the threshold is higher than the available witness count, it becomes impossible
    /// to ever collect enough witness proofs to satisfy the requirement. This would make
    /// the DID permanently unable to process new log entries, so it must be caught early
    /// during configuration validation.
    #[test]
    fn test_validate_threshold_exceeds_witnesses_error() {
        let w = Witnesses::Value {
            threshold: 3,
            witnesses: vec![Witness {
                id: Multibase::new("w1"),
            }],
        };
        let err = w.validate().unwrap_err();
        assert!(err.to_string().contains("less than the threshold"));
    }

    /// Tests that a valid witness configuration passes validation successfully.
    ///
    /// A configuration with a threshold of 1 and one witness node represents the
    /// simplest valid witness setup. This confirms that well-formed configurations
    /// are accepted, ensuring DID WebVH log entries can proceed with proper witness
    /// attestation.
    #[test]
    fn test_validate_ok() {
        let w = Witnesses::Value {
            threshold: 1,
            witnesses: vec![Witness {
                id: Multibase::new("w1"),
            }],
        };
        assert!(w.validate().is_ok());
    }

    /// Tests the `witnesses()` and `threshold()` accessor methods for both variants.
    ///
    /// The `Empty` variant must return `None` for both accessors, while the `Value`
    /// variant must return the configured witness list and threshold. Callers depend
    /// on these accessors to inspect witness parameters when constructing and verifying
    /// DID WebVH log entries, so correct behavior for both variants is essential.
    #[test]
    fn test_witnesses_accessors() {
        let empty = Witnesses::Empty {};
        assert!(empty.witnesses().is_none());
        assert!(empty.threshold().is_none());

        let value = Witnesses::Value {
            threshold: 2,
            witnesses: vec![
                Witness {
                    id: Multibase::new("w1"),
                },
                Witness {
                    id: Multibase::new("w2"),
                },
            ],
        };
        assert_eq!(value.witnesses().unwrap().len(), 2);
        assert_eq!(value.threshold(), Some(2));
    }

    /// Tests `as_did()` with a raw multibase key (prepends `did:key:` prefix).
    #[test]
    fn test_witness_as_did_from_raw_key() {
        let w = Witness {
            id: Multibase::new("z6Mktest"),
        };
        assert_eq!(w.as_did(), "did:key:z6Mktest");
    }

    /// Tests `as_did()` with a full `did:key:` DID (returns as-is, no double prefix).
    #[test]
    fn test_witness_as_did_from_full_did() {
        let w = Witness {
            id: Multibase::new("did:key:z6Mktest"),
        };
        assert_eq!(w.as_did(), "did:key:z6Mktest");
    }

    /// Tests `as_did_key()` with a raw multibase key.
    #[test]
    fn test_witness_as_did_key_from_raw_key() {
        let w = Witness {
            id: Multibase::new("z6Mktest"),
        };
        assert_eq!(w.as_did_key(), "did:key:z6Mktest#z6Mktest");
    }

    /// Tests `as_did_key()` with a full `did:key:` DID (extracts raw key for fragment).
    #[test]
    fn test_witness_as_did_key_from_full_did() {
        let w = Witness {
            id: Multibase::new("did:key:z6Mktest"),
        };
        assert_eq!(w.as_did_key(), "did:key:z6Mktest#z6Mktest");
    }

    /// Tests the `Display` trait implementation for `Witness`.
    ///
    /// The display output should be the raw witness ID (the multibase-encoded public key)
    /// without any DID prefix. This is important for logging, debugging, and serialization
    /// contexts where the compact identifier form is preferred over the full DID URI.
    #[test]
    fn test_witness_display() {
        let w = Witness {
            id: Multibase::new("z6Mktest"),
        };
        assert_eq!(format!("{}", w), "z6Mktest");
    }

    /// Tests the `is_empty()` method across all meaningful states.
    ///
    /// Covers three cases: the `Empty` variant (always empty), a `Value` variant with
    /// an empty witness list (logically empty despite being the `Value` variant), and a
    /// `Value` variant with witnesses (not empty). Correctly detecting emptiness is
    /// critical because DID WebVH validation logic uses `is_empty()` as a guard before
    /// performing witness-related checks on log entries.
    #[test]
    fn test_is_empty() {
        assert!(Witnesses::Empty {}.is_empty());
        assert!(
            Witnesses::Value {
                threshold: 0,
                witnesses: vec![]
            }
            .is_empty()
        );
        assert!(
            !Witnesses::Value {
                threshold: 1,
                witnesses: vec![Witness {
                    id: Multibase::new("w1")
                }],
            }
            .is_empty()
        );
    }

    // ===== WitnessesBuilder tests =====

    #[test]
    fn builder_valid_single_witness() {
        let w = Witnesses::builder()
            .threshold(1)
            .witness(Multibase::new("z6Mktest"))
            .build();
        assert!(w.is_ok());
        let w = w.unwrap();
        assert_eq!(w.threshold(), Some(1));
        assert_eq!(w.witnesses().unwrap().len(), 1);
    }

    #[test]
    fn builder_valid_multiple_witnesses() {
        let w = Witnesses::builder()
            .threshold(2)
            .witnesses(vec![
                Multibase::new("z6Mk1"),
                Multibase::new("z6Mk2"),
                Multibase::new("z6Mk3"),
            ])
            .build();
        assert!(w.is_ok());
        assert_eq!(w.unwrap().witnesses().unwrap().len(), 3);
    }

    #[test]
    fn builder_threshold_zero_error() {
        let result = Witnesses::builder()
            .threshold(0)
            .witness(Multibase::new("z6Mk1"))
            .build();
        assert!(result.is_err());
    }

    #[test]
    fn builder_threshold_exceeds_witnesses_error() {
        let result = Witnesses::builder()
            .threshold(3)
            .witness(Multibase::new("z6Mk1"))
            .build();
        assert!(result.is_err());
    }
}