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 PostGuard IRMA attribute, which is a simple case of an IRMA ConDisCon.
30#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, PartialEq, Eq, Clone, Default)]
31pub struct Attribute {
32    #[serde(rename = "t")]
33    /// Attribute type.
34    pub atype: String,
35
36    /// Attribute value.
37    #[serde(rename = "v")]
38    pub value: Option<String>,
39}
40
41/// An PostGuard policy used to encapsulate a shared secret for one recipient.
42#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
43pub struct Policy {
44    /// Timestamp (UNIX time).
45    #[serde(rename = "ts")]
46    pub timestamp: u64,
47
48    /// A conjunction of attributes.
49    pub con: Vec<Attribute>,
50}
51
52/// An PostGuard hidden policy.
53///
54/// A policy where (part of) the value of the attributes is hidden.
55/// This type is safe for usage in (public) [Header][`crate::client::Header`] alongside the ciphertext.
56#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
57pub struct HiddenPolicy {
58    /// Timestamp (UNIX time).
59    #[serde(rename = "ts")]
60    pub timestamp: u64,
61
62    /// A conjunction of attributes, with redacted values.
63    pub con: Vec<Attribute>,
64}
65
66impl Attribute {
67    fn hintify_value(&self) -> Attribute {
68        let hidden_value = self.value.as_ref().map(|v| {
69            if HINT_TYPES.contains(&&self.atype[..]) {
70                let (begin, end) = v.split_at(v.len().saturating_sub(AMOUNT_CHARS_TO_HIDE));
71                format!("{begin}{}", "*".repeat(end.len()))
72            } else {
73                "".to_string()
74            }
75        });
76
77        Attribute {
78            atype: self.atype.clone(),
79            value: hidden_value,
80        }
81    }
82}
83
84impl Policy {
85    /// Completely hides the attribute value, or provides a hint for certain attribute types
86    pub fn to_hidden(&self) -> HiddenPolicy {
87        HiddenPolicy {
88            timestamp: self.timestamp,
89            con: self.con.iter().map(Attribute::hintify_value).collect(),
90        }
91    }
92
93    /// Derives an 64-byte identity from a [`Policy`].
94    pub fn derive(&self) -> Result<[u8; 64], Error> {
95        // This method implements domain separation as follows:
96        // Suppose we have the following policy:
97        //  - con[0..n - 1] consisting of n conjunctions.
98        //  - timestamp
99        // = H(0 || f_0 || f'_0 ||  .. || f_{n-1} || f'_{n-1} || timestamp),
100        // where f_i  = H(2i + 1 || a.typ.len() || a.typ),
101        // and   f'_i = H(2i + 2 || a.val.len() || a.val).
102        //
103        // Conjunction is sorted. This requires that Attribute implements a stable Ord.
104        // Since lengths encoded as usize are not platform-agnostic, we convert all
105        // usize to u64.
106
107        if self.con.len() > MAX_CON {
108            return Err(Error::ConstraintViolation);
109        }
110
111        let mut tmp = [0u8; 64];
112        let mut pre_h = Sha3::v512();
113
114        // 0 indicates the IRMA authentication method.
115        pre_h.update(&[0x00]);
116
117        let mut copy = self.con.clone();
118        copy.sort();
119
120        for (i, ar) in copy.iter().enumerate() {
121            let mut f = Sha3::v512();
122
123            f.update(&((2 * i + 1) as u64).to_be_bytes());
124            let at_bytes = ar.atype.as_bytes();
125            f.update(&(at_bytes.len() as u64).to_be_bytes());
126            f.update(at_bytes);
127            f.finalize(&mut tmp);
128
129            pre_h.update(&tmp);
130
131            // Initialize a new hash, f'
132            f = Sha3::v512();
133            f.update(&((2 * i + 2) as u64).to_be_bytes());
134
135            match &ar.value {
136                None => f.update(&IDENTITY_UNSET.to_be_bytes()),
137                Some(val) => {
138                    let val_bytes = val.as_bytes();
139                    f.update(&(val_bytes.len() as u64).to_be_bytes());
140                    f.update(val_bytes);
141                }
142            }
143
144            f.finalize(&mut tmp);
145            pre_h.update(&tmp);
146        }
147
148        pre_h.update(&self.timestamp.to_be_bytes());
149        let mut res = [0u8; 64];
150        pre_h.finalize(&mut res);
151
152        Ok(res)
153    }
154
155    /// Derive a KEM identity from a [`Policy`].
156    pub fn derive_kem<K: IBKEM>(&self) -> Result<<K as IBKEM>::Id, Error> {
157        Ok(<K as IBKEM>::Id::derive(&self.derive()?))
158    }
159
160    /// Derive an IBS identity from a [`Policy`].
161    pub fn derive_ibs(&self) -> Result<ibs::gg::Identity, Error> {
162        Ok(Identity::from(&self.derive()?))
163    }
164}
165
166impl Attribute {
167    /// Construct a new attribute request.
168    pub fn new(atype: &str, value: Option<&str>) -> Self {
169        let atype = atype.to_string();
170        let value = value.map(|s| s.to_string());
171
172        Attribute { atype, value }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::identity::{Attribute, Policy};
179    use crate::test::TestSetup;
180    use alloc::string::ToString;
181    use alloc::vec::Vec;
182    use ibe::kem::cgw_kv::CGWKV;
183
184    #[test]
185    fn test_ordering() {
186        let mut rng = rand::thread_rng();
187        // Test that symantically equivalent policies map to the same IBE identity.
188        let setup = TestSetup::new(&mut rng);
189
190        let policies: Vec<Policy> = setup.policy.into_values().collect();
191        let p1_derived = policies[1].derive_kem::<CGWKV>().unwrap();
192
193        let mut reversed = policies[1].clone();
194        reversed.con.reverse();
195        assert_eq!(&p1_derived, &reversed.derive_kem::<CGWKV>().unwrap());
196
197        // The timestamp should matter, and therefore map to a different IBE identity.
198        reversed.timestamp += 1;
199        assert_ne!(&p1_derived, &reversed.derive_kem::<CGWKV>().unwrap());
200    }
201
202    #[test]
203    fn test_hints() {
204        let attr = Attribute {
205            atype: "pbdf.sidn-pbdf.mobilenumber.mobilenumber".to_string(),
206            value: Some("123456789".to_string()),
207        };
208        let hinted = attr.hintify_value();
209        assert_eq!(hinted.value, Some("12345****".to_string()));
210
211        let attr_short = Attribute {
212            atype: "pbdf.sidn-pbdf.mobilenumber.mobilenumber".to_string(),
213            value: Some("123".to_string()),
214        };
215        let hinted_short = attr_short.hintify_value();
216        assert_eq!(hinted_short.value, Some("***".to_string()));
217
218        let attr_not_whitelisted = Attribute {
219            atype: "pbdf.sidn-pbdf.mobilenumber.test".to_string(),
220            value: Some("123456789".to_string()),
221        };
222        let hinted_empty = attr_not_whitelisted.hintify_value();
223        assert_eq!(hinted_empty.value, Some("".to_string()));
224    }
225
226    #[test]
227    fn test_regression() {
228        let mut rng = rand::thread_rng();
229        let setup = TestSetup::new(&mut rng);
230
231        // Make sure that the policies in the TestSetup map to identical KEM/IBS identities.
232        let kem_ids: [[u8; 64]; 5] = [
233            [
234                243, 215, 91, 185, 176, 144, 186, 190, 101, 135, 237, 186, 47, 183, 76, 243, 182,
235                195, 213, 35, 18, 38, 203, 7, 53, 157, 78, 193, 99, 141, 169, 0, 13, 112, 111, 32,
236                172, 75, 5, 106, 165, 47, 53, 111, 177, 2, 8, 107, 242, 252, 49, 241, 67, 229, 5,
237                191, 13, 17, 246, 216, 119, 186, 227, 119,
238            ],
239            [
240                245, 162, 197, 104, 15, 166, 248, 109, 79, 173, 252, 30, 92, 165, 193, 237, 255,
241                228, 162, 5, 42, 227, 151, 207, 97, 134, 20, 41, 20, 142, 220, 5, 234, 222, 45,
242                199, 163, 191, 112, 167, 52, 193, 120, 143, 245, 8, 24, 46, 8, 77, 183, 255, 32,
243                196, 251, 247, 233, 114, 16, 114, 69, 19, 88, 105,
244            ],
245            [
246                55, 240, 138, 50, 172, 20, 36, 194, 154, 137, 247, 125, 112, 215, 118, 219, 172,
247                226, 21, 87, 116, 226, 44, 228, 62, 148, 86, 82, 119, 154, 209, 89, 219, 49, 115,
248                130, 187, 57, 252, 108, 239, 118, 210, 165, 13, 53, 96, 200, 55, 211, 229, 32, 59,
249                140, 234, 87, 124, 64, 128, 223, 6, 248, 172, 238,
250            ],
251            [
252                224, 26, 15, 201, 109, 47, 252, 119, 219, 216, 15, 186, 65, 123, 47, 131, 130, 196,
253                248, 145, 241, 235, 13, 216, 182, 74, 236, 81, 198, 67, 28, 7, 114, 158, 252, 90,
254                123, 131, 138, 155, 56, 93, 46, 93, 160, 8, 72, 122, 193, 229, 123, 36, 69, 50,
255                189, 38, 183, 208, 7, 102, 249, 33, 219, 46,
256            ],
257            [
258                199, 241, 225, 34, 158, 92, 56, 128, 249, 122, 93, 192, 132, 106, 3, 247, 209, 109,
259                66, 92, 203, 108, 184, 198, 208, 254, 255, 150, 116, 17, 225, 112, 114, 121, 189,
260                231, 19, 215, 46, 246, 250, 211, 61, 254, 172, 44, 242, 18, 170, 49, 37, 56, 140,
261                217, 127, 97, 247, 210, 224, 181, 220, 246, 126, 140,
262            ],
263        ];
264
265        let ibs_ids: [[u8; 32]; 5] = [
266            [
267                180, 14, 93, 181, 36, 29, 110, 232, 226, 36, 52, 230, 202, 168, 128, 63, 18, 200,
268                133, 234, 142, 171, 42, 130, 204, 102, 83, 232, 69, 19, 188, 40,
269            ],
270            [
271                28, 98, 33, 83, 107, 211, 195, 182, 119, 220, 223, 113, 224, 225, 193, 22, 200,
272                249, 124, 48, 182, 122, 0, 65, 241, 201, 164, 104, 236, 175, 50, 108,
273            ],
274            [
275                254, 181, 235, 14, 113, 97, 93, 200, 45, 48, 184, 245, 237, 118, 89, 250, 199, 105,
276                213, 208, 27, 41, 189, 166, 246, 1, 105, 163, 244, 239, 78, 122,
277            ],
278            [
279                165, 205, 240, 238, 241, 135, 30, 175, 42, 99, 93, 112, 171, 40, 249, 246, 133,
280                162, 228, 144, 133, 77, 246, 199, 134, 77, 78, 182, 224, 66, 111, 239,
281            ],
282            [
283                22, 61, 147, 117, 0, 147, 225, 164, 134, 216, 244, 108, 165, 173, 205, 236, 24,
284                185, 73, 128, 9, 95, 91, 162, 155, 120, 67, 252, 138, 112, 249, 217,
285            ],
286        ];
287
288        for (p, (kem, ibs)) in setup
289            .policies
290            .iter()
291            .zip(kem_ids.iter().zip(ibs_ids.iter()))
292        {
293            let kem2 = p.derive_kem::<CGWKV>().unwrap();
294            let ibs2 = p.derive_ibs().unwrap();
295
296            assert_eq!(&kem[..], &kem2.0);
297            assert_eq!(&ibs::gg::Identity::from(&ibs), &ibs2);
298        }
299    }
300}