baseid_core/
claims.rs

1//! Unified claim types for multi-format credential lifecycle.
2//!
3//! `ClaimSet` provides a namespace-keyed claim structure that maps naturally
4//! to all credential formats: SD-JWT (flat claims), W3C VC (credential subject),
5//! mdoc (namespaced data elements), and BBS+ (ordered message list).
6//!
7//! `DisclosureSelection` controls which claims are revealed, hidden, or proven
8//! via zero-knowledge predicates during credential presentation.
9
10use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15/// A namespace-keyed set of claims.
16///
17/// The outer key is a namespace (e.g., `"org.iso.18013.5.1"` for mdoc,
18/// `""` or `"vc"` for flat formats like SD-JWT and W3C VC). The inner map
19/// holds claim name to value pairs.
20///
21/// # Examples
22///
23/// ```
24/// use baseid_core::claims::ClaimSet;
25/// use serde_json::json;
26///
27/// let mut claims = ClaimSet::new();
28/// claims.insert("", "given_name", json!("Alice"));
29/// claims.insert("", "family_name", json!("Smith"));
30/// claims.insert("", "birth_date", json!("1990-01-15"));
31///
32/// assert_eq!(claims.get("", "given_name"), Some(&json!("Alice")));
33/// ```
34#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
35pub struct ClaimSet {
36    namespaces: BTreeMap<String, BTreeMap<String, Value>>,
37}
38
39impl ClaimSet {
40    /// Create an empty claim set.
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Insert a claim into the given namespace.
46    pub fn insert(&mut self, namespace: &str, name: &str, value: Value) {
47        self.namespaces
48            .entry(namespace.to_string())
49            .or_default()
50            .insert(name.to_string(), value);
51    }
52
53    /// Get a claim value by namespace and name.
54    pub fn get(&self, namespace: &str, name: &str) -> Option<&Value> {
55        self.namespaces.get(namespace)?.get(name)
56    }
57
58    /// Iterate over all namespaces and their claims.
59    pub fn namespaces(&self) -> impl Iterator<Item = (&str, &BTreeMap<String, Value>)> {
60        self.namespaces.iter().map(|(k, v)| (k.as_str(), v))
61    }
62
63    /// Get claims in a specific namespace.
64    pub fn namespace(&self, ns: &str) -> Option<&BTreeMap<String, Value>> {
65        self.namespaces.get(ns)
66    }
67
68    /// Returns the total number of claims across all namespaces.
69    pub fn len(&self) -> usize {
70        self.namespaces.values().map(|m| m.len()).sum()
71    }
72
73    /// Returns true if there are no claims.
74    pub fn is_empty(&self) -> bool {
75        self.namespaces.values().all(|m| m.is_empty())
76    }
77
78    /// Convert flat-namespace claims to a `serde_json::Value` object.
79    ///
80    /// If a default namespace (`""`) exists, its claims are returned as a
81    /// flat JSON object. Otherwise, namespaces are nested.
82    pub fn to_json(&self) -> Value {
83        if self.namespaces.len() == 1 {
84            if let Some(claims) = self.namespaces.get("") {
85                return Value::Object(claims.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
86            }
87        }
88        serde_json::to_value(&self.namespaces).unwrap_or(Value::Null)
89    }
90
91    /// Convert claims in the default namespace to a flat JSON object.
92    ///
93    /// Returns `None` if there is no default namespace (`""`).
94    /// Claims in other namespaces are ignored.
95    pub fn to_json_flat(&self) -> Option<Value> {
96        let claims = self.namespaces.get("")?;
97        Some(Value::Object(
98            claims.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
99        ))
100    }
101
102    /// Convert the full claim set to a namespaced JSON object.
103    ///
104    /// Always returns `{"namespace": {"claim": value, ...}, ...}` regardless
105    /// of how many namespaces exist. Use this when you need deterministic
106    /// structure (e.g., for mdoc with multiple namespaces).
107    pub fn to_json_namespaced(&self) -> Value {
108        serde_json::to_value(&self.namespaces).unwrap_or(Value::Null)
109    }
110
111    /// Create a ClaimSet from a flat JSON object (uses default namespace `""`).
112    pub fn from_json(value: &Value) -> Option<Self> {
113        let obj = value.as_object()?;
114        let mut cs = Self::new();
115        for (k, v) in obj {
116            cs.insert("", k, v.clone());
117        }
118        Some(cs)
119    }
120}
121
122/// A path identifying a specific claim, optionally within a namespace.
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub struct ClaimPath {
125    /// Namespace (empty string for default/flat namespace).
126    pub namespace: String,
127    /// Claim name within the namespace.
128    pub name: String,
129}
130
131impl ClaimPath {
132    /// Create a claim path in the default namespace.
133    pub fn new(name: &str) -> Self {
134        Self {
135            namespace: String::new(),
136            name: name.to_string(),
137        }
138    }
139
140    /// Create a claim path in a specific namespace.
141    pub fn with_namespace(namespace: &str, name: &str) -> Self {
142        Self {
143            namespace: namespace.to_string(),
144            name: name.to_string(),
145        }
146    }
147}
148
149/// How a specific claim should be disclosed during presentation.
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
151pub enum ClaimDisclosure {
152    /// Reveal the claim value to the verifier.
153    Reveal,
154    /// Prove a predicate about the claim without revealing its value.
155    /// Only supported by ZK-capable formats (BBS+, AnonCreds).
156    Predicate(PredicateType),
157    /// Do not disclose this claim at all.
158    Hide,
159}
160
161/// Zero-knowledge predicates that can be proven about a claim value.
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163pub enum PredicateType {
164    /// Prove claim value > threshold (e.g., age > 18).
165    GreaterThan(Value),
166    /// Prove claim value >= threshold.
167    GreaterThanOrEqual(Value),
168    /// Prove claim value < threshold (e.g., birth_date < "2008-03-01").
169    LessThan(Value),
170    /// Prove claim value <= threshold.
171    LessThanOrEqual(Value),
172    /// Prove claim value is one of the given set.
173    InSet(Vec<Value>),
174    /// Prove claim value is NOT one of the given set.
175    NotInSet(Vec<Value>),
176    /// Prove the credential has not been revoked (via accumulator witness).
177    NonRevoked,
178}
179
180/// Selection of which claims to reveal, hide, or prove predicates about.
181///
182/// Used by `CredentialPresenter::present()` to control selective disclosure.
183///
184/// # Examples
185///
186/// ```
187/// use baseid_core::claims::{DisclosureSelection, ClaimDisclosure, PredicateType};
188/// use serde_json::json;
189///
190/// let disclosure = DisclosureSelection::new()
191///     .set("given_name", ClaimDisclosure::Reveal)
192///     .set("family_name", ClaimDisclosure::Reveal)
193///     .set("birth_date", ClaimDisclosure::Predicate(
194///         PredicateType::LessThan(json!("2008-03-01"))
195///     ))
196///     .set("address", ClaimDisclosure::Hide);
197/// ```
198#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
199pub struct DisclosureSelection {
200    selections: BTreeMap<String, BTreeMap<String, ClaimDisclosure>>,
201}
202
203impl DisclosureSelection {
204    /// Create an empty disclosure selection.
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Set disclosure for a claim in the default namespace.
210    pub fn set(mut self, name: &str, disclosure: ClaimDisclosure) -> Self {
211        self.selections
212            .entry(String::new())
213            .or_default()
214            .insert(name.to_string(), disclosure);
215        self
216    }
217
218    /// Set disclosure for a claim in a specific namespace.
219    pub fn set_namespaced(
220        mut self,
221        namespace: &str,
222        name: &str,
223        disclosure: ClaimDisclosure,
224    ) -> Self {
225        self.selections
226            .entry(namespace.to_string())
227            .or_default()
228            .insert(name.to_string(), disclosure);
229        self
230    }
231
232    /// Convenience: reveal a claim in the default namespace.
233    pub fn reveal(self, name: &str) -> Self {
234        self.set(name, ClaimDisclosure::Reveal)
235    }
236
237    /// Convenience: hide a claim in the default namespace.
238    pub fn hide(self, name: &str) -> Self {
239        self.set(name, ClaimDisclosure::Hide)
240    }
241
242    /// Convenience: set a predicate for a claim in the default namespace.
243    pub fn predicate(self, name: &str, predicate: PredicateType) -> Self {
244        self.set(name, ClaimDisclosure::Predicate(predicate))
245    }
246
247    /// Get the disclosure selection for a claim.
248    pub fn get(&self, namespace: &str, name: &str) -> Option<&ClaimDisclosure> {
249        self.selections.get(namespace)?.get(name)
250    }
251
252    /// Iterate over all selections grouped by namespace.
253    pub fn iter(&self) -> impl Iterator<Item = (&str, &str, &ClaimDisclosure)> {
254        self.selections.iter().flat_map(|(ns, claims)| {
255            claims
256                .iter()
257                .map(move |(name, disc)| (ns.as_str(), name.as_str(), disc))
258        })
259    }
260
261    /// Returns true if any selection uses a predicate.
262    pub fn has_predicates(&self) -> bool {
263        self.iter()
264            .any(|(_, _, d)| matches!(d, ClaimDisclosure::Predicate(_)))
265    }
266
267    /// Returns all claims marked as Reveal in the default namespace.
268    pub fn revealed_claims(&self) -> Vec<&str> {
269        self.selections
270            .get("")
271            .map(|claims| {
272                claims
273                    .iter()
274                    .filter(|(_, d)| matches!(d, ClaimDisclosure::Reveal))
275                    .map(|(name, _)| name.as_str())
276                    .collect()
277            })
278            .unwrap_or_default()
279    }
280
281    /// Returns all claims marked as Hide in the default namespace.
282    pub fn hidden_claims(&self) -> Vec<&str> {
283        self.selections
284            .get("")
285            .map(|claims| {
286                claims
287                    .iter()
288                    .filter(|(_, d)| matches!(d, ClaimDisclosure::Hide))
289                    .map(|(name, _)| name.as_str())
290                    .collect()
291            })
292            .unwrap_or_default()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use serde_json::json;
300
301    #[test]
302    fn claim_set_basic_operations() {
303        let mut cs = ClaimSet::new();
304        assert!(cs.is_empty());
305        assert_eq!(cs.len(), 0);
306
307        cs.insert("", "given_name", json!("Alice"));
308        cs.insert("", "family_name", json!("Smith"));
309        cs.insert("", "age", json!(30));
310
311        assert_eq!(cs.len(), 3);
312        assert!(!cs.is_empty());
313        assert_eq!(cs.get("", "given_name"), Some(&json!("Alice")));
314        assert_eq!(cs.get("", "age"), Some(&json!(30)));
315        assert_eq!(cs.get("", "nonexistent"), None);
316        assert_eq!(cs.get("other_ns", "given_name"), None);
317    }
318
319    #[test]
320    fn claim_set_namespaced() {
321        let mut cs = ClaimSet::new();
322        cs.insert("org.iso.18013.5.1", "family_name", json!("Smith"));
323        cs.insert("org.iso.18013.5.1", "given_name", json!("Alice"));
324        cs.insert("org.iso.18013.5.1.aamva", "DHS_compliance", json!("F"));
325
326        assert_eq!(cs.len(), 3);
327        assert_eq!(
328            cs.get("org.iso.18013.5.1", "family_name"),
329            Some(&json!("Smith"))
330        );
331        assert_eq!(
332            cs.get("org.iso.18013.5.1.aamva", "DHS_compliance"),
333            Some(&json!("F"))
334        );
335    }
336
337    #[test]
338    fn claim_set_json_roundtrip() {
339        let mut cs = ClaimSet::new();
340        cs.insert("", "name", json!("Alice"));
341        cs.insert("", "age", json!(30));
342
343        let json_val = cs.to_json();
344        let obj = json_val.as_object().unwrap();
345        assert_eq!(obj.get("name"), Some(&json!("Alice")));
346        assert_eq!(obj.get("age"), Some(&json!(30)));
347
348        let cs2 = ClaimSet::from_json(&json_val).unwrap();
349        assert_eq!(cs, cs2);
350    }
351
352    #[test]
353    fn disclosure_selection_builder() {
354        let ds = DisclosureSelection::new()
355            .reveal("given_name")
356            .reveal("family_name")
357            .predicate("birth_date", PredicateType::LessThan(json!("2008-03-01")))
358            .hide("address");
359
360        assert_eq!(ds.get("", "given_name"), Some(&ClaimDisclosure::Reveal));
361        assert_eq!(ds.get("", "family_name"), Some(&ClaimDisclosure::Reveal));
362        assert_eq!(
363            ds.get("", "birth_date"),
364            Some(&ClaimDisclosure::Predicate(PredicateType::LessThan(json!(
365                "2008-03-01"
366            ))))
367        );
368        assert_eq!(ds.get("", "address"), Some(&ClaimDisclosure::Hide));
369        assert!(ds.has_predicates());
370    }
371
372    #[test]
373    fn disclosure_selection_no_predicates() {
374        let ds = DisclosureSelection::new().reveal("name").hide("secret");
375
376        assert!(!ds.has_predicates());
377        assert_eq!(ds.revealed_claims(), vec!["name"]);
378        assert_eq!(ds.hidden_claims(), vec!["secret"]);
379    }
380
381    #[test]
382    fn disclosure_selection_namespaced() {
383        let ds = DisclosureSelection::new()
384            .set_namespaced("org.iso.18013.5.1", "family_name", ClaimDisclosure::Reveal)
385            .set_namespaced("org.iso.18013.5.1", "portrait", ClaimDisclosure::Hide);
386
387        assert_eq!(
388            ds.get("org.iso.18013.5.1", "family_name"),
389            Some(&ClaimDisclosure::Reveal)
390        );
391        assert_eq!(
392            ds.get("org.iso.18013.5.1", "portrait"),
393            Some(&ClaimDisclosure::Hide)
394        );
395    }
396
397    #[test]
398    fn predicate_types_serialize() {
399        let predicates = vec![
400            PredicateType::GreaterThan(json!(18)),
401            PredicateType::GreaterThanOrEqual(json!(18)),
402            PredicateType::LessThan(json!("2008-03-01")),
403            PredicateType::LessThanOrEqual(json!("2008-03-01")),
404            PredicateType::InSet(vec![json!("CA"), json!("US")]),
405            PredicateType::NotInSet(vec![json!("banned")]),
406            PredicateType::NonRevoked,
407        ];
408
409        for p in &predicates {
410            let serialized = serde_json::to_string(p).unwrap();
411            let deserialized: PredicateType = serde_json::from_str(&serialized).unwrap();
412            assert_eq!(p, &deserialized);
413        }
414    }
415
416    #[test]
417    fn to_json_flat_default_namespace() {
418        let mut cs = ClaimSet::new();
419        cs.insert("", "name", json!("Alice"));
420        cs.insert("", "age", json!(30));
421
422        let flat = cs.to_json_flat().unwrap();
423        let obj = flat.as_object().unwrap();
424        assert_eq!(obj.get("name"), Some(&json!("Alice")));
425        assert_eq!(obj.get("age"), Some(&json!(30)));
426    }
427
428    #[test]
429    fn to_json_flat_no_default_namespace() {
430        let mut cs = ClaimSet::new();
431        cs.insert("org.iso.18013.5.1", "family_name", json!("Smith"));
432
433        assert!(cs.to_json_flat().is_none());
434    }
435
436    #[test]
437    fn to_json_flat_ignores_other_namespaces() {
438        let mut cs = ClaimSet::new();
439        cs.insert("", "name", json!("Alice"));
440        cs.insert("org.iso.18013.5.1", "family_name", json!("Smith"));
441
442        let flat = cs.to_json_flat().unwrap();
443        let obj = flat.as_object().unwrap();
444        assert_eq!(obj.len(), 1);
445        assert_eq!(obj.get("name"), Some(&json!("Alice")));
446    }
447
448    #[test]
449    fn to_json_namespaced_always_nests() {
450        let mut cs = ClaimSet::new();
451        cs.insert("", "name", json!("Alice"));
452
453        let ns = cs.to_json_namespaced();
454        let obj = ns.as_object().unwrap();
455        // Even a single default namespace is nested under ""
456        assert!(obj.contains_key(""));
457        let inner = obj.get("").unwrap().as_object().unwrap();
458        assert_eq!(inner.get("name"), Some(&json!("Alice")));
459    }
460
461    #[test]
462    fn to_json_namespaced_multiple() {
463        let mut cs = ClaimSet::new();
464        cs.insert("ns1", "a", json!(1));
465        cs.insert("ns2", "b", json!(2));
466
467        let ns = cs.to_json_namespaced();
468        let obj = ns.as_object().unwrap();
469        assert!(obj.contains_key("ns1"));
470        assert!(obj.contains_key("ns2"));
471    }
472
473    #[test]
474    fn claim_path_default_namespace() {
475        let path = ClaimPath::new("given_name");
476        assert_eq!(path.namespace, "");
477        assert_eq!(path.name, "given_name");
478    }
479
480    #[test]
481    fn claim_path_with_namespace() {
482        let path = ClaimPath::with_namespace("org.iso.18013.5.1", "family_name");
483        assert_eq!(path.namespace, "org.iso.18013.5.1");
484        assert_eq!(path.name, "family_name");
485    }
486}