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}