Skip to main content

pg_core/
identity.rs

1//! Identity definitions and utilities.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::vec::Vec;
7use ibs::gg::Identity;
8
9use crate::error::Error;
10use ibe::kem::IBKEM;
11use ibe::Derive;
12use serde::{Deserialize, Serialize};
13use tiny_keccak::{Hasher, Sha3};
14
15const IDENTITY_UNSET: u64 = u64::MAX;
16const MAX_CON: usize = (IDENTITY_UNSET as usize - 1) >> 1;
17const AMOUNT_CHARS_TO_HIDE: usize = 4;
18const HINT_TYPES: &[&str] = &[
19    "pbdf.sidn-pbdf.mobilenumber.mobilenumber",
20    "pbdf.pbdf.surfnet-2.id",
21    "pbdf.nuts.agb.agbcode",
22    "irma-demo.sidn-pbdf.mobilenumber.mobilenumber",
23    "irma-demo.nuts.agb.agbcode",
24];
25
26/// The complete encryption policy for all recipients.
27pub type EncryptionPolicy = BTreeMap<String, Policy>;
28
29/// A canonicalization rule for an attribute value.
30///
31/// The canonical form is not ours to choose: the PKG derives a user secret key
32/// from the value Yivi discloses, so a sender's policy only decrypts when its
33/// bytes match that disclosure exactly. These rules mirror the form Yivi
34/// stores; they do not define it.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36enum Rule {
37    /// Trim surrounding whitespace and lowercase. Yivi stores email addresses
38    /// in lower case, which is why a capitalized address fails to decrypt.
39    Email,
40
41    /// Remove grouping separators and map an international `00` prefix to `+`.
42    /// Yivi stores mobile numbers in canonical E.164.
43    Phone,
44}
45
46/// Attribute types carrying a canonicalization rule, keyed by the
47/// credential-and-attribute *tail* of the type rather than the whole string, so
48/// that every scheme — `pbdf.`, `irma-demo.` and any future one — matches the
49/// same entry. Keying on full literals is how [`HINT_TYPES`] above ended up
50/// covering `irma-demo` for mobile numbers but not for email.
51const RULES: &[(&str, Rule)] = &[
52    ("sidn-pbdf.email.email", Rule::Email),
53    ("sidn-pbdf.mobilenumber.mobilenumber", Rule::Phone),
54];
55
56/// Non-whitespace characters used to group the digits of a written phone
57/// number.
58///
59/// Whitespace is deliberately **not** listed here: it is removed beforehand by
60/// `char::is_whitespace`, which covers every space, tab and newline rather than
61/// the handful anyone thinks to enumerate. Listing a space here as well would
62/// invite the next reader to add `\t` to this list instead, which is how the
63/// gap arose in the first place.
64const PHONE_SEPARATORS: &[char] = &['-', '\u{2011}', '\u{2013}', '(', ')', '.', '/'];
65
66/// The rule for an attribute type, if it has one.
67fn rule_for(atype: &str) -> Option<Rule> {
68    RULES.iter().find_map(|(tail, rule)| {
69        let is_match = atype == *tail
70            || (atype.len() > tail.len()
71                && atype.ends_with(tail)
72                && atype.as_bytes()[atype.len() - tail.len() - 1] == b'.');
73
74        is_match.then_some(*rule)
75    })
76}
77
78/// Canonicalizes an attribute value for the given attribute type.
79///
80/// Values of a type that carries no rule are returned unchanged. This function
81/// is **total**: a value it cannot bring into canonical form is passed through
82/// untouched rather than rejected, because the same rule runs on the PKG's side
83/// over a Yivi disclosure, where a rejection would lock a recipient out of
84/// their own message. A bare national phone number is the case that stays
85/// unfixed — resolving it needs a country, which this crate does not have. Use
86/// [`is_canonical`] to detect it and report it to the user.
87///
88/// The rule is idempotent: `canonicalize(t, canonicalize(t, v))` equals
89/// `canonicalize(t, v)`.
90///
91/// ```
92/// use pg_core::identity::canonicalize;
93///
94/// let t = "pbdf.sidn-pbdf.email.email";
95/// assert_eq!(canonicalize(t, " Alice@Example.COM "), "alice@example.com");
96///
97/// let t = "pbdf.sidn-pbdf.mobilenumber.mobilenumber";
98/// assert_eq!(canonicalize(t, "+31 6 1234 5678"), "+31612345678");
99/// ```
100pub fn canonicalize(atype: &str, value: &str) -> String {
101    match rule_for(atype) {
102        Some(Rule::Email) => value.trim().to_lowercase(),
103        Some(Rule::Phone) => {
104            // Whitespace goes first, for two reasons. A pasted value routinely
105            // carries a tab or a trailing newline, and `Rule::Email` trims
106            // where this rule otherwise would not. And it makes the trunk group
107            // below recognisable when it is written spaced out, `( 0 )`.
108            let compact: String = value.chars().filter(|c| !c.is_whitespace()).collect();
109
110            // "+31 (0)6 ..." writes the national trunk prefix in parentheses,
111            // and E.164 drops it. Remove the whole group before separators are
112            // stripped: strip the parentheses first and the 0 survives into a
113            // number that looks valid but dials nowhere.
114            let compact = compact.replace("(0)", "");
115
116            let compact: String = compact
117                .chars()
118                .filter(|c| !PHONE_SEPARATORS.contains(c))
119                .collect();
120
121            match compact.strip_prefix("00") {
122                Some(rest) => {
123                    let mut e164 = String::with_capacity(rest.len() + 1);
124                    e164.push('+');
125                    e164.push_str(rest);
126                    e164
127                }
128                None => compact,
129            }
130        }
131        None => value.to_string(),
132    }
133}
134
135/// Whether a value is already in the canonical form its attribute type expects.
136///
137/// Types without a rule are always canonical. For a phone number this is
138/// stricter than "[`canonicalize`] would not change it": the result must also
139/// be valid E.164 — including long enough to carry a subscriber number, not
140/// just a country code — which is what lets a client reject the bare national
141/// number that [`canonicalize`] cannot repair.
142///
143/// ```
144/// use pg_core::identity::is_canonical;
145///
146/// let t = "pbdf.sidn-pbdf.mobilenumber.mobilenumber";
147/// assert!(is_canonical(t, "+31612345678"));
148/// assert!(!is_canonical(t, "0612345678")); // needs a country to resolve
149/// ```
150pub fn is_canonical(atype: &str, value: &str) -> bool {
151    match rule_for(atype) {
152        // The second conjunct is currently implied by the first — an E.164
153        // value carries nothing for the rule to strip — and is kept so this
154        // predicate states the whole invariant rather than resting on a proof
155        // about the rule as it stands today.
156        Some(Rule::Phone) => is_e164(value) && canonicalize(atype, value) == value,
157        Some(_) => canonicalize(atype, value) == value,
158        None => true,
159    }
160}
161
162/// The shortest assigned E.164 number: a three-digit country code and a
163/// four-digit subscriber number, as used by Saint Helena (`+290`) and Niue
164/// (`+683`).
165///
166/// The floor exists because `is_canonical` is what a policy editor asks before
167/// letting a value through. Accepting `1` would make a bare country code a
168/// green light, and `canonicalize` reaches one from ordinary input: `0031`
169/// becomes `+31`.
170const E164_MIN_DIGITS: usize = 7;
171
172/// The E.164 ceiling: fifteen digits including the country code.
173const E164_MAX_DIGITS: usize = 15;
174
175/// Whether a string is a valid E.164 number: `+`, a non-zero leading digit, and
176/// [`E164_MIN_DIGITS`] to [`E164_MAX_DIGITS`] digits in total.
177fn is_e164(value: &str) -> bool {
178    let Some(digits) = value.strip_prefix('+') else {
179        return false;
180    };
181
182    let len = digits.len();
183
184    (E164_MIN_DIGITS..=E164_MAX_DIGITS).contains(&len)
185        && !digits.starts_with('0')
186        && digits.bytes().all(|b| b.is_ascii_digit())
187}
188
189/// A PostGuard IRMA attribute, which is a simple case of an IRMA ConDisCon.
190#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, PartialEq, Eq, Clone, Default)]
191pub struct Attribute {
192    #[serde(rename = "t")]
193    /// Attribute type.
194    pub atype: String,
195
196    /// Attribute value.
197    #[serde(rename = "v")]
198    pub value: Option<String>,
199}
200
201/// An PostGuard policy used to encapsulate a shared secret for one recipient.
202#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
203pub struct Policy {
204    /// Timestamp (UNIX time).
205    #[serde(rename = "ts")]
206    pub timestamp: u64,
207
208    /// A conjunction of attributes.
209    pub con: Vec<Attribute>,
210}
211
212/// An PostGuard hidden policy.
213///
214/// A policy where (part of) the value of the attributes is hidden.
215/// This type is safe for usage in (public) [Header][`crate::client::Header`] alongside the ciphertext.
216#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
217pub struct HiddenPolicy {
218    /// Timestamp (UNIX time).
219    #[serde(rename = "ts")]
220    pub timestamp: u64,
221
222    /// A conjunction of attributes, with redacted values.
223    pub con: Vec<Attribute>,
224}
225
226impl Attribute {
227    fn hintify_value(&self) -> Attribute {
228        let hidden_value = self.value.as_ref().map(|v| {
229            if HINT_TYPES.contains(&&self.atype[..]) {
230                let (begin, end) = v.split_at(v.len().saturating_sub(AMOUNT_CHARS_TO_HIDE));
231                format!("{begin}{}", "*".repeat(end.len()))
232            } else {
233                "".to_string()
234            }
235        });
236
237        Attribute {
238            atype: self.atype.clone(),
239            value: hidden_value,
240        }
241    }
242
243    /// Rewrites this attribute's value into the canonical form for its type.
244    ///
245    /// See [`canonicalize`] for what each type's rule does.
246    pub fn canonicalize(&mut self) {
247        if let Some(value) = &self.value {
248            let canonical = canonicalize(&self.atype, value);
249
250            if canonical != *value {
251                self.value = Some(canonical);
252            }
253        }
254    }
255}
256
257impl Policy {
258    /// Rewrites every attribute value in this policy into its canonical form.
259    ///
260    /// See [`canonicalize`] for what each type's rule does.
261    pub fn canonicalize(&mut self) {
262        for attribute in &mut self.con {
263            attribute.canonicalize();
264        }
265    }
266
267    /// Returns a copy of this policy with every attribute value canonicalized.
268    pub fn canonical(&self) -> Policy {
269        let mut policy = self.clone();
270        policy.canonicalize();
271
272        policy
273    }
274
275    /// Completely hides the attribute value, or provides a hint for certain attribute types
276    pub fn to_hidden(&self) -> HiddenPolicy {
277        HiddenPolicy {
278            timestamp: self.timestamp,
279            con: self.con.iter().map(Attribute::hintify_value).collect(),
280        }
281    }
282
283    /// Derives an 64-byte identity from a [`Policy`].
284    ///
285    /// Attribute values are canonicalized first (see [`canonicalize`]), so a
286    /// sender who typed `Alice@Example.com` and a PKG holding Yivi's
287    /// `alice@example.com` arrive at the same identity. Because both sides
288    /// apply the same function, this only ever merges identities that used to
289    /// differ — it cannot split one that already matched.
290    pub fn derive(&self) -> Result<[u8; 64], Error> {
291        // This method implements domain separation as follows:
292        // Suppose we have the following policy:
293        //  - con[0..n - 1] consisting of n conjunctions.
294        //  - timestamp
295        // = H(0 || f_0 || f'_0 ||  .. || f_{n-1} || f'_{n-1} || timestamp),
296        // where f_i  = H(2i + 1 || a.typ.len() || a.typ),
297        // and   f'_i = H(2i + 2 || a.val.len() || a.val).
298        //
299        // Conjunction is sorted. This requires that Attribute implements a stable Ord.
300        // Since lengths encoded as usize are not platform-agnostic, we convert all
301        // usize to u64.
302
303        if self.con.len() > MAX_CON {
304            return Err(Error::ConstraintViolation);
305        }
306
307        let mut tmp = [0u8; 64];
308        let mut pre_h = Sha3::v512();
309
310        // 0 indicates the IRMA authentication method.
311        pre_h.update(&[0x00]);
312
313        // Canonicalize before sorting: a rule can change a value, and therefore
314        // the ordering. Doing this here rather than only at construction means
315        // a policy assembled by hand, deserialized, or built by the PKG from a
316        // Yivi disclosure derives the same identity as one the sealer wrote —
317        // there is no construction site left to forget.
318        let mut copy = self.con.clone();
319        for ar in &mut copy {
320            ar.canonicalize();
321        }
322        copy.sort();
323
324        for (i, ar) in copy.iter().enumerate() {
325            let mut f = Sha3::v512();
326
327            f.update(&((2 * i + 1) as u64).to_be_bytes());
328            let at_bytes = ar.atype.as_bytes();
329            f.update(&(at_bytes.len() as u64).to_be_bytes());
330            f.update(at_bytes);
331            f.finalize(&mut tmp);
332
333            pre_h.update(&tmp);
334
335            // Initialize a new hash, f'
336            f = Sha3::v512();
337            f.update(&((2 * i + 2) as u64).to_be_bytes());
338
339            match &ar.value {
340                None => f.update(&IDENTITY_UNSET.to_be_bytes()),
341                Some(val) => {
342                    let val_bytes = val.as_bytes();
343                    f.update(&(val_bytes.len() as u64).to_be_bytes());
344                    f.update(val_bytes);
345                }
346            }
347
348            f.finalize(&mut tmp);
349            pre_h.update(&tmp);
350        }
351
352        pre_h.update(&self.timestamp.to_be_bytes());
353        let mut res = [0u8; 64];
354        pre_h.finalize(&mut res);
355
356        Ok(res)
357    }
358
359    /// Derive a KEM identity from a [`Policy`].
360    pub fn derive_kem<K: IBKEM>(&self) -> Result<<K as IBKEM>::Id, Error> {
361        Ok(<K as IBKEM>::Id::derive(&self.derive()?))
362    }
363
364    /// Derive an IBS identity from a [`Policy`].
365    pub fn derive_ibs(&self) -> Result<ibs::gg::Identity, Error> {
366        Ok(Identity::from(&self.derive()?))
367    }
368}
369
370impl Attribute {
371    /// Construct a new attribute request.
372    pub fn new(atype: &str, value: Option<&str>) -> Self {
373        let atype = atype.to_string();
374        let value = value.map(|s| s.to_string());
375
376        Attribute { atype, value }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use crate::identity::{canonicalize, is_canonical, Attribute, Policy};
383    use crate::test::TestSetup;
384    use alloc::string::ToString;
385    use alloc::vec::Vec;
386    use ibe::kem::cgw_kv::CGWKV;
387
388    const EMAIL: &str = "pbdf.sidn-pbdf.email.email";
389    const PHONE: &str = "pbdf.sidn-pbdf.mobilenumber.mobilenumber";
390
391    /// The canonicalization contract: `(attribute type, input, canonical form)`.
392    ///
393    /// Every row is also asserted to be idempotent. That property is
394    /// load-bearing rather than incidental: the rule runs once when the sealer
395    /// stores a policy and again when `derive` hashes it, so a rule that moved
396    /// a value on the second application would break the very identities it
397    /// exists to align.
398    const VECTORS: &[(&str, &str, &str)] = &[
399        // Email: trim, then lowercase. Yivi stores addresses in lower case.
400        (EMAIL, "Alice@Example.COM", "alice@example.com"),
401        (EMAIL, "  alice@example.com  ", "alice@example.com"),
402        (EMAIL, "\u{00a0}Alice@Example.com\n", "alice@example.com"),
403        (EMAIL, "alice@example.com", "alice@example.com"),
404        // Email: the local part is lowercased too. RFC 5321 calls it
405        // case-sensitive, but Yivi does not, and Yivi is what the PKG sees.
406        (
407            EMAIL,
408            "AliceCarroll@example.com",
409            "alicecarroll@example.com",
410        ),
411        // Phone: strip grouping separators, map an international 00 prefix.
412        (PHONE, "+31 6 1234 5678", "+31612345678"),
413        (PHONE, "+31-6-1234-5678", "+31612345678"),
414        // The trunk-prefix group goes, rather than just its parentheses: a
415        // surviving 0 yields "+310612345678", which passes an E.164 shape
416        // check and dials nowhere.
417        (PHONE, "+31 (0)6 12345678", "+31612345678"),
418        (PHONE, "0031612345678", "+31612345678"),
419        (PHONE, "+31612345678", "+31612345678"),
420        // Whitespace is stripped wholesale, not from an enumerated list. A
421        // pasted number carries a tab or a trailing newline, and the digit
422        // groups of a written one are as often U+2007 FIGURE SPACE or U+2009
423        // THIN SPACE as they are a plain space.
424        (PHONE, "\t+31612345678\n", "+31612345678"),
425        (PHONE, "+31\u{2007}6\u{2009}1234 5678", "+31612345678"),
426        (PHONE, "  +31 6 1234 5678  ", "+31612345678"),
427        // The trunk group is recognised spaced out, which is why whitespace
428        // has to go before the group is removed rather than with the other
429        // separators. Left in place it yields "+310612345678", which passes an
430        // E.164 shape check and dials nowhere.
431        (PHONE, "+31 ( 0 ) 6 12345678", "+31612345678"),
432        // Phone: a bare national number needs a country to resolve, which this
433        // crate does not have. It passes through untouched rather than being
434        // rejected — `is_canonical` is what reports it.
435        (PHONE, "0612345678", "0612345678"),
436        (PHONE, "06 1234 5678", "0612345678"),
437        // A type with no rule is never touched, whatever its value looks like.
438        ("pbdf.gemeente.personalData.name", "Bob", "Bob"),
439        ("pbdf.gemeente.personalData.name", "  Bob  ", "  Bob  "),
440        ("test.test.email", "Alice@Example.COM", "Alice@Example.COM"),
441    ];
442
443    #[test]
444    fn test_canonicalization_vectors() {
445        for (atype, input, expected) in VECTORS {
446            let once = canonicalize(atype, input);
447            assert_eq!(&once, expected, "canonicalize({atype}, {input:?})");
448
449            // Idempotence, asserted for every row rather than a chosen few.
450            let twice = canonicalize(atype, &once);
451            assert_eq!(twice, once, "canonicalize is not idempotent on {input:?}");
452        }
453    }
454
455    #[test]
456    fn test_canonicalization_matches_every_scheme() {
457        // Keying on the type's tail is what makes the rule apply in irma-demo
458        // and any future scheme, rather than only in the one someone remembered
459        // to list. `HINT_TYPES` above is keyed on full literals and is missing
460        // its irma-demo email row for exactly that reason.
461        for scheme in ["pbdf", "irma-demo", "some-future-scheme"] {
462            let atype = alloc::format!("{scheme}.sidn-pbdf.email.email");
463            assert_eq!(
464                canonicalize(&atype, "Alice@Example.COM"),
465                "alice@example.com"
466            );
467        }
468
469        // A tail match is not a substring match: the boundary must be a dot.
470        assert_eq!(
471            canonicalize("pbdf.notsidn-pbdf.email.email", "Alice@Example.COM"),
472            "Alice@Example.COM"
473        );
474    }
475
476    #[test]
477    fn test_is_canonical() {
478        // Types without a rule are canonical by definition.
479        assert!(is_canonical("pbdf.gemeente.personalData.name", "  Bob  "));
480
481        assert!(is_canonical(EMAIL, "alice@example.com"));
482        assert!(!is_canonical(EMAIL, "Alice@Example.com"));
483
484        // Phone is stricter than "canonicalize would not change it": a bare
485        // national number is a fixed point of the rule but not valid E.164,
486        // which is the case a client has to be able to report.
487        assert!(is_canonical(PHONE, "+31612345678"));
488        assert!(!is_canonical(PHONE, "0612345678"));
489        assert_eq!(canonicalize(PHONE, "0612345678"), "0612345678");
490
491        // E.164 bounds: 7 to 15 digits, no leading zero after the +.
492        assert!(is_canonical(PHONE, "+123456789012345"));
493        assert!(!is_canonical(PHONE, "+1234567890123456"));
494        assert!(!is_canonical(PHONE, "+0612345678"));
495        assert!(!is_canonical(PHONE, "+"));
496        assert!(!is_canonical(PHONE, "31612345678"));
497
498        // A bare country code is not a green light, and it is reachable from
499        // ordinary input rather than hypothetical: "0031" canonicalizes to it.
500        assert_eq!(canonicalize(PHONE, "0031"), "+31");
501        assert!(!is_canonical(PHONE, "+31"));
502
503        // The floor is the shortest assigned number, not a round guess.
504        assert!(is_canonical(PHONE, "+2904256")); // Saint Helena, 7 digits
505        assert!(!is_canonical(PHONE, "+290425")); // one short
506    }
507
508    #[test]
509    fn test_canonicalization_merges_identities_never_splits_them() {
510        // The property the whole design rests on: because the sender and the
511        // PKG apply the same function, canonicalization is a coarsening of
512        // equality. Values that used to derive differently can now agree; two
513        // values that already agreed cannot be pulled apart.
514        let raw = Policy {
515            timestamp: 1_700_000_000,
516            con: alloc::vec![Attribute::new(EMAIL, Some("Alice@Example.COM"))],
517        };
518        let canonical = Policy {
519            timestamp: 1_700_000_000,
520            con: alloc::vec![Attribute::new(EMAIL, Some("alice@example.com"))],
521        };
522
523        assert_eq!(raw.derive().unwrap(), canonical.derive().unwrap());
524
525        // An untouched type still separates what it always separated.
526        let bob = Policy {
527            timestamp: 1_700_000_000,
528            con: alloc::vec![Attribute::new(
529                "pbdf.gemeente.personalData.name",
530                Some("Bob")
531            )],
532        };
533        let bob_upper = Policy {
534            timestamp: 1_700_000_000,
535            con: alloc::vec![Attribute::new(
536                "pbdf.gemeente.personalData.name",
537                Some("BOB")
538            )],
539        };
540
541        assert_ne!(bob.derive().unwrap(), bob_upper.derive().unwrap());
542    }
543
544    #[test]
545    fn test_policy_canonicalization_reaches_the_wire() {
546        // `derive` canonicalizing is not enough on its own: the *sender*
547        // signing policy travels in full as `SignatureExt.pol`, and an older
548        // verifier derives the signer's identity from those bytes, so the stored
549        // value has to move too.
550        //
551        // Recipient policies are the other case and they behave differently: a
552        // header stores `Policy::to_hidden`, which blanks the value outright for
553        // any type outside `HINT_TYPES`, so a recipient's value never reaches
554        // the wire unredacted and no reader derives from it.
555        let mut policy = Policy {
556            timestamp: 1_700_000_000,
557            con: alloc::vec![
558                Attribute::new(EMAIL, Some(" Alice@Example.COM ")),
559                Attribute::new(PHONE, Some("+31 6 1234 5678")),
560            ],
561        };
562        policy.canonicalize();
563
564        assert_eq!(policy.con[0].value.as_deref(), Some("alice@example.com"));
565        assert_eq!(policy.con[1].value.as_deref(), Some("+31612345678"));
566
567        // An attribute with no value is left alone rather than gaining one.
568        let mut valueless = Policy {
569            timestamp: 0,
570            con: alloc::vec![Attribute::new(EMAIL, None)],
571        };
572        valueless.canonicalize();
573        assert_eq!(valueless.con[0].value, None);
574    }
575
576    #[test]
577    fn test_ordering() {
578        let mut rng = rand::thread_rng();
579        // Test that symantically equivalent policies map to the same IBE identity.
580        let setup = TestSetup::new(&mut rng);
581
582        let policies: Vec<Policy> = setup.policy.into_values().collect();
583        let p1_derived = policies[1].derive_kem::<CGWKV>().unwrap();
584
585        let mut reversed = policies[1].clone();
586        reversed.con.reverse();
587        assert_eq!(&p1_derived, &reversed.derive_kem::<CGWKV>().unwrap());
588
589        // The timestamp should matter, and therefore map to a different IBE identity.
590        reversed.timestamp += 1;
591        assert_ne!(&p1_derived, &reversed.derive_kem::<CGWKV>().unwrap());
592    }
593
594    #[test]
595    fn test_hints() {
596        let attr = Attribute {
597            atype: "pbdf.sidn-pbdf.mobilenumber.mobilenumber".to_string(),
598            value: Some("123456789".to_string()),
599        };
600        let hinted = attr.hintify_value();
601        assert_eq!(hinted.value, Some("12345****".to_string()));
602
603        let attr_short = Attribute {
604            atype: "pbdf.sidn-pbdf.mobilenumber.mobilenumber".to_string(),
605            value: Some("123".to_string()),
606        };
607        let hinted_short = attr_short.hintify_value();
608        assert_eq!(hinted_short.value, Some("***".to_string()));
609
610        let attr_not_whitelisted = Attribute {
611            atype: "pbdf.sidn-pbdf.mobilenumber.test".to_string(),
612            value: Some("123456789".to_string()),
613        };
614        let hinted_empty = attr_not_whitelisted.hintify_value();
615        assert_eq!(hinted_empty.value, Some("".to_string()));
616    }
617
618    #[test]
619    fn test_regression() {
620        let mut rng = rand::thread_rng();
621        let setup = TestSetup::new(&mut rng);
622
623        // Make sure that the policies in the TestSetup map to identical KEM/IBS identities.
624        let kem_ids: [[u8; 64]; 5] = [
625            [
626                243, 215, 91, 185, 176, 144, 186, 190, 101, 135, 237, 186, 47, 183, 76, 243, 182,
627                195, 213, 35, 18, 38, 203, 7, 53, 157, 78, 193, 99, 141, 169, 0, 13, 112, 111, 32,
628                172, 75, 5, 106, 165, 47, 53, 111, 177, 2, 8, 107, 242, 252, 49, 241, 67, 229, 5,
629                191, 13, 17, 246, 216, 119, 186, 227, 119,
630            ],
631            [
632                245, 162, 197, 104, 15, 166, 248, 109, 79, 173, 252, 30, 92, 165, 193, 237, 255,
633                228, 162, 5, 42, 227, 151, 207, 97, 134, 20, 41, 20, 142, 220, 5, 234, 222, 45,
634                199, 163, 191, 112, 167, 52, 193, 120, 143, 245, 8, 24, 46, 8, 77, 183, 255, 32,
635                196, 251, 247, 233, 114, 16, 114, 69, 19, 88, 105,
636            ],
637            [
638                55, 240, 138, 50, 172, 20, 36, 194, 154, 137, 247, 125, 112, 215, 118, 219, 172,
639                226, 21, 87, 116, 226, 44, 228, 62, 148, 86, 82, 119, 154, 209, 89, 219, 49, 115,
640                130, 187, 57, 252, 108, 239, 118, 210, 165, 13, 53, 96, 200, 55, 211, 229, 32, 59,
641                140, 234, 87, 124, 64, 128, 223, 6, 248, 172, 238,
642            ],
643            [
644                224, 26, 15, 201, 109, 47, 252, 119, 219, 216, 15, 186, 65, 123, 47, 131, 130, 196,
645                248, 145, 241, 235, 13, 216, 182, 74, 236, 81, 198, 67, 28, 7, 114, 158, 252, 90,
646                123, 131, 138, 155, 56, 93, 46, 93, 160, 8, 72, 122, 193, 229, 123, 36, 69, 50,
647                189, 38, 183, 208, 7, 102, 249, 33, 219, 46,
648            ],
649            [
650                199, 241, 225, 34, 158, 92, 56, 128, 249, 122, 93, 192, 132, 106, 3, 247, 209, 109,
651                66, 92, 203, 108, 184, 198, 208, 254, 255, 150, 116, 17, 225, 112, 114, 121, 189,
652                231, 19, 215, 46, 246, 250, 211, 61, 254, 172, 44, 242, 18, 170, 49, 37, 56, 140,
653                217, 127, 97, 247, 210, 224, 181, 220, 246, 126, 140,
654            ],
655        ];
656
657        let ibs_ids: [[u8; 32]; 5] = [
658            [
659                180, 14, 93, 181, 36, 29, 110, 232, 226, 36, 52, 230, 202, 168, 128, 63, 18, 200,
660                133, 234, 142, 171, 42, 130, 204, 102, 83, 232, 69, 19, 188, 40,
661            ],
662            [
663                28, 98, 33, 83, 107, 211, 195, 182, 119, 220, 223, 113, 224, 225, 193, 22, 200,
664                249, 124, 48, 182, 122, 0, 65, 241, 201, 164, 104, 236, 175, 50, 108,
665            ],
666            [
667                254, 181, 235, 14, 113, 97, 93, 200, 45, 48, 184, 245, 237, 118, 89, 250, 199, 105,
668                213, 208, 27, 41, 189, 166, 246, 1, 105, 163, 244, 239, 78, 122,
669            ],
670            [
671                165, 205, 240, 238, 241, 135, 30, 175, 42, 99, 93, 112, 171, 40, 249, 246, 133,
672                162, 228, 144, 133, 77, 246, 199, 134, 77, 78, 182, 224, 66, 111, 239,
673            ],
674            [
675                22, 61, 147, 117, 0, 147, 225, 164, 134, 216, 244, 108, 165, 173, 205, 236, 24,
676                185, 73, 128, 9, 95, 91, 162, 155, 120, 67, 252, 138, 112, 249, 217,
677            ],
678        ];
679
680        for (p, (kem, ibs)) in setup
681            .policies
682            .iter()
683            .zip(kem_ids.iter().zip(ibs_ids.iter()))
684        {
685            let kem2 = p.derive_kem::<CGWKV>().unwrap();
686            let ibs2 = p.derive_ibs().unwrap();
687
688            assert_eq!(&kem[..], &kem2.0);
689            assert_eq!(&ibs::gg::Identity::from(&ibs), &ibs2);
690        }
691    }
692}