Skip to main content

auths_verifier/
types.rs

1//! Verification types: reports, statuses, and device DIDs.
2
3use crate::witness::WitnessQuorum;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7// ============================================================================
8// Verification Report Types
9// ============================================================================
10
11/// Machine-readable verification result containing status, chain details, and warnings.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct VerificationReport {
14    /// The overall verification status
15    pub status: VerificationStatus,
16    /// Details of each link in the verification chain
17    pub chain: Vec<ChainLink>,
18    /// Non-fatal warnings encountered during verification
19    pub warnings: Vec<String>,
20    /// Optional witness quorum result (present when witness verification was performed)
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub witness_quorum: Option<WitnessQuorum>,
23}
24
25impl VerificationReport {
26    /// Returns true only when the verification status is Valid.
27    pub fn is_valid(&self) -> bool {
28        matches!(self.status, VerificationStatus::Valid)
29    }
30
31    /// Creates a new valid VerificationReport with the given chain.
32    pub fn valid(chain: Vec<ChainLink>) -> Self {
33        Self {
34            status: VerificationStatus::Valid,
35            chain,
36            warnings: Vec::new(),
37            witness_quorum: None,
38        }
39    }
40
41    /// Creates a new VerificationReport with the given status and chain.
42    pub fn with_status(status: VerificationStatus, chain: Vec<ChainLink>) -> Self {
43        Self {
44            status,
45            chain,
46            warnings: Vec::new(),
47            witness_quorum: None,
48        }
49    }
50}
51
52/// Verification outcome indicating success or the type of failure.
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(tag = "type")]
55pub enum VerificationStatus {
56    /// The attestation(s) are valid
57    Valid,
58    /// The attestation has expired
59    Expired {
60        /// When the attestation expired
61        at: DateTime<Utc>,
62    },
63    /// The attestation has been revoked
64    Revoked {
65        /// When the attestation was revoked (if known)
66        at: Option<DateTime<Utc>>,
67    },
68    /// A signature in the chain is invalid
69    InvalidSignature {
70        /// The step in the chain where the invalid signature was found (0-indexed)
71        step: usize,
72    },
73    /// The chain has a broken link (issuer→subject mismatch or missing attestation)
74    BrokenChain {
75        /// Description of the missing link
76        missing_link: String,
77    },
78    /// Insufficient witness receipts to meet quorum threshold
79    InsufficientWitnesses {
80        /// Number of witnesses required
81        required: usize,
82        /// Number of witnesses that verified successfully
83        verified: usize,
84    },
85}
86
87/// A single link in a verification chain, representing one attestation's verification result.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct ChainLink {
90    /// The issuer DID of this attestation
91    pub issuer: String,
92    /// The subject DID of this attestation
93    pub subject: String,
94    /// Whether this link's signature is valid
95    pub valid: bool,
96    /// Error message if verification failed
97    pub error: Option<String>,
98}
99
100impl ChainLink {
101    /// Creates a new valid chain link.
102    pub fn valid(issuer: String, subject: String) -> Self {
103        Self {
104            issuer,
105            subject,
106            valid: true,
107            error: None,
108        }
109    }
110
111    /// Creates a new invalid chain link with an error message.
112    pub fn invalid(issuer: String, subject: String, error: String) -> Self {
113        Self {
114            issuer,
115            subject,
116            valid: false,
117            error: Some(error),
118        }
119    }
120}
121
122// ============================================================================
123// DID Types
124// ============================================================================
125
126use std::borrow::Borrow;
127use std::fmt;
128use std::ops::Deref;
129
130// ============================================================================
131// IdentityDID Type
132// ============================================================================
133
134/// Strongly-typed wrapper for identity DIDs (e.g., `"did:keri:E..."`).
135///
136/// Usage:
137/// ```ignore
138/// let did = IdentityDID::new("did:keri:Eabc123");
139/// assert_eq!(did.as_str(), "did:keri:Eabc123");
140///
141/// let s: String = did.into_inner();
142/// ```
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
144#[repr(transparent)]
145pub struct IdentityDID(pub String);
146
147impl IdentityDID {
148    /// Create a new `IdentityDID` from a raw string.
149    pub fn new<S: Into<String>>(s: S) -> Self {
150        Self(s.into())
151    }
152
153    /// Wraps a DID string without validation (for trusted internal paths).
154    pub fn new_unchecked(s: String) -> Self {
155        Self(s)
156    }
157
158    /// Returns the DID as a string slice.
159    pub fn as_str(&self) -> &str {
160        &self.0
161    }
162
163    /// Consumes self and returns the inner String.
164    pub fn into_inner(self) -> String {
165        self.0
166    }
167}
168
169impl fmt::Display for IdentityDID {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.write_str(&self.0)
172    }
173}
174
175impl From<&str> for IdentityDID {
176    fn from(s: &str) -> Self {
177        Self(s.to_string())
178    }
179}
180
181impl From<String> for IdentityDID {
182    fn from(s: String) -> Self {
183        Self(s)
184    }
185}
186
187impl From<IdentityDID> for String {
188    fn from(did: IdentityDID) -> String {
189        did.0
190    }
191}
192
193impl Deref for IdentityDID {
194    type Target = str;
195
196    fn deref(&self) -> &Self::Target {
197        &self.0
198    }
199}
200
201impl AsRef<str> for IdentityDID {
202    fn as_ref(&self) -> &str {
203        &self.0
204    }
205}
206
207impl Borrow<str> for IdentityDID {
208    fn borrow(&self) -> &str {
209        &self.0
210    }
211}
212
213impl PartialEq<str> for IdentityDID {
214    fn eq(&self, other: &str) -> bool {
215        self.0 == other
216    }
217}
218
219impl PartialEq<&str> for IdentityDID {
220    fn eq(&self, other: &&str) -> bool {
221        self.0 == *other
222    }
223}
224
225impl PartialEq<IdentityDID> for str {
226    fn eq(&self, other: &IdentityDID) -> bool {
227        self == other.0
228    }
229}
230
231impl PartialEq<IdentityDID> for &str {
232    fn eq(&self, other: &IdentityDID) -> bool {
233        *self == other.0
234    }
235}
236
237// ============================================================================
238// DeviceDID Type
239// ============================================================================
240
241/// Wrapper around a device DID string that ensures Git-safe ref formatting.
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
243pub struct DeviceDID(pub String);
244
245impl DeviceDID {
246    /// Create a new `DeviceDID` from a raw string.
247    pub fn new<S: Into<String>>(s: S) -> Self {
248        DeviceDID(s.into())
249    }
250
251    /// Constructs a `did:key:z...` identifier from a 32-byte Ed25519 public key.
252    ///
253    /// This uses the multicodec prefix for Ed25519 (0xED 0x01) and encodes it with base58btc.
254    pub fn from_ed25519(pubkey: &[u8; 32]) -> Self {
255        let mut prefixed = vec![0xED, 0x01];
256        prefixed.extend_from_slice(pubkey);
257
258        let encoded = bs58::encode(prefixed).into_string();
259        Self(format!("did:key:z{}", encoded))
260    }
261
262    /// Returns a sanitized version of the DID for use in Git refs,
263    /// replacing all non-alphanumeric characters with `_`.
264    pub fn ref_name(&self) -> String {
265        self.0
266            .chars()
267            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
268            .collect()
269    }
270
271    /// Compares a sanitized DID ref name to this real DeviceDID.
272    /// Used to match Git refs to known device DIDs.
273    pub fn matches_sanitized_ref(&self, ref_name: &str) -> bool {
274        self.ref_name() == ref_name
275    }
276
277    /// Tries to reverse-lookup a real DID from a sanitized string,
278    /// given a list of known real DIDs.
279    pub fn from_sanitized<'a>(
280        sanitized: &str,
281        known_dids: &'a [DeviceDID],
282    ) -> Option<&'a DeviceDID> {
283        known_dids.iter().find(|did| did.ref_name() == sanitized)
284    }
285
286    /// Optionally expose the inner raw DID
287    pub fn as_str(&self) -> &str {
288        &self.0
289    }
290}
291
292// Allow `.to_string()` and printing
293impl fmt::Display for DeviceDID {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        self.0.fmt(f)
296    }
297}
298
299// Allow `DeviceDID::from("did:key:abc")` and vice versa
300impl From<&str> for DeviceDID {
301    fn from(s: &str) -> Self {
302        DeviceDID(s.to_string())
303    }
304}
305
306impl From<String> for DeviceDID {
307    fn from(s: String) -> Self {
308        DeviceDID(s)
309    }
310}
311
312// Optionally deref to &str
313impl Deref for DeviceDID {
314    type Target = str;
315
316    fn deref(&self) -> &Self::Target {
317        &self.0
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::keri::Said;
325
326    #[test]
327    fn report_without_witness_quorum_deserializes() {
328        // JSON from before witness_quorum field existed
329        let json = r#"{
330            "status": {"type": "Valid"},
331            "chain": [],
332            "warnings": []
333        }"#;
334        let report: VerificationReport = serde_json::from_str(json).unwrap();
335        assert!(report.is_valid());
336        assert!(report.witness_quorum.is_none());
337    }
338
339    #[test]
340    fn insufficient_witnesses_serializes_correctly() {
341        let status = VerificationStatus::InsufficientWitnesses {
342            required: 3,
343            verified: 1,
344        };
345        let json = serde_json::to_string(&status).unwrap();
346        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
347        assert_eq!(parsed["type"], "InsufficientWitnesses");
348        assert_eq!(parsed["required"], 3);
349        assert_eq!(parsed["verified"], 1);
350
351        // Roundtrip
352        let roundtripped: VerificationStatus = serde_json::from_str(&json).unwrap();
353        assert_eq!(roundtripped, status);
354    }
355
356    #[test]
357    fn report_with_witness_quorum_roundtrips() {
358        use crate::witness::{WitnessQuorum, WitnessReceiptResult};
359
360        let report = VerificationReport {
361            status: VerificationStatus::Valid,
362            chain: vec![],
363            warnings: vec![],
364            witness_quorum: Some(WitnessQuorum {
365                required: 2,
366                verified: 2,
367                receipts: vec![
368                    WitnessReceiptResult {
369                        witness_id: "did:key:w1".into(),
370                        receipt_said: Said::new_unchecked("EReceipt1".into()),
371                        verified: true,
372                    },
373                    WitnessReceiptResult {
374                        witness_id: "did:key:w2".into(),
375                        receipt_said: Said::new_unchecked("EReceipt2".into()),
376                        verified: true,
377                    },
378                ],
379            }),
380        };
381
382        let json = serde_json::to_string(&report).unwrap();
383        let parsed: VerificationReport = serde_json::from_str(&json).unwrap();
384        assert_eq!(report, parsed);
385        assert!(parsed.witness_quorum.is_some());
386        assert_eq!(parsed.witness_quorum.unwrap().verified, 2);
387    }
388
389    #[test]
390    fn report_without_witness_quorum_skips_in_json() {
391        let report = VerificationReport::valid(vec![]);
392        let json = serde_json::to_string(&report).unwrap();
393        // witness_quorum should be omitted from JSON when None
394        assert!(!json.contains("witness_quorum"));
395    }
396}