Skip to main content

pubhubs/servers/auths/
card.rs

1//! User endpoints related to the issuance of cards
2use crate::api;
3use crate::api::OpenError;
4use crate::attr;
5use crate::handle::Handle;
6use crate::misc::time_ext;
7use crate::servers::yivi;
8
9use actix_web::web;
10
11use std::collections::{BTreeSet, VecDeque};
12use std::rc::Rc;
13
14use super::server::*;
15
16/// Configuration of PubHubs card issuance
17#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
18#[serde(deny_unknown_fields)]
19pub struct CardConfig {
20    /// What type of card to issue
21    #[serde(rename = "type")]
22    #[serde(default)]
23    pub card_type: CardType,
24
25    /// The attribute type associated to the pubhubs card
26    #[serde(default = "default_card_attr_type")]
27    pub card_attr_type: Handle,
28
29    /// For how long is a PubHubs card valid?
30    #[serde(default)]
31    pub valid_for: CardValidFor,
32
33    /// What registration source to use.  Default: [`crate::servers::Config::phc_url`].
34    ///
35    /// Use [`App::registration_source()`] to get the default.
36    #[serde(default)]
37    registration_source: Option<String>,
38}
39
40/// Configuration of PubHubs card issuance
41#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
42#[serde(untagged)]
43pub enum CardValidFor {
44    Historic(BTreeSet<HistoricCardValidFor>),
45
46    /// To be deprecated
47    Simple(#[serde(with = "time_ext::human_duration")] core::time::Duration),
48}
49
50impl Default for CardValidFor {
51    fn default() -> Self {
52        CardValidFor::Historic(Default::default()) // empty BTreeSet
53    }
54}
55
56impl CardValidFor {
57    /// Returns the duration for which a card issued now is valid
58    pub fn now(&self) -> core::time::Duration {
59        self.at(api::NumericDate::now())
60    }
61
62    /// Returns the duration for which a card issued at the given moment is valid
63    pub fn at(&self, moment: api::NumericDate) -> core::time::Duration {
64        match self {
65            CardValidFor::Simple(duration) => *duration,
66            CardValidFor::Historic(historic) => {
67                historic
68                    // of all historic entries ..
69                    .range(
70                        // that were at or before the yivi epoch of the given moment ..
71                        ..=HistoricCardValidFor {
72                            starting_epoch: yivi::Epoch::from(moment),
73                            value: core::time::Duration::MAX,
74                        },
75                    )
76                    // pick the latest..
77                    .next_back()
78                    .copied()
79                    // if there is a latest
80                    .unwrap_or_default()
81                    .value
82            }
83        }
84    }
85
86    pub fn to_welcome_ep_format(&self) -> Option<Vec<api::auths::HistoricCardValidity>> {
87        let CardValidFor::Historic(historic) = self else {
88            return None;
89        };
90
91        let mut list: VecDeque<api::auths::HistoricCardValidity> = historic
92            .iter()
93            .map(|hcvf| api::auths::HistoricCardValidity {
94                starting_at_timestamp: hcvf.starting_epoch.starts(),
95                card_valid_for_secs: hcvf.value.as_secs(),
96            })
97            .collect();
98
99        let unix_epoch = api::NumericDate::new(0);
100
101        'epoch_present: {
102            if let Some(hcv) = list.front()
103                && hcv.starting_at_timestamp == unix_epoch
104            {
105                break 'epoch_present;
106            }
107
108            list.push_front(api::auths::HistoricCardValidity {
109                starting_at_timestamp: unix_epoch,
110                card_valid_for_secs: self.at(unix_epoch).as_secs(),
111            });
112        }
113
114        Some(list.into())
115    }
116}
117
118/// Configuration of PubHubs card issuance
119#[derive(
120    serde::Deserialize, serde::Serialize, Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq,
121)]
122#[serde(deny_unknown_fields)]
123pub struct HistoricCardValidFor {
124    // NOTE: do not change the order of these fields; derive(PartialOrd) depends on it
125    /// Starting which yivi epoch did the card validity have this value?
126    pub starting_epoch: yivi::Epoch,
127
128    #[serde(with = "time_ext::human_duration")]
129    pub value: core::time::Duration,
130}
131
132impl Default for HistoricCardValidFor {
133    fn default() -> Self {
134        Self {
135            starting_epoch: yivi::Epoch::with_seqnr(0),
136            value: core::time::Duration::from_secs(2 * 7 * 24 * 3600), // two weeks
137        }
138    }
139}
140
141fn default_card_attr_type() -> Handle {
142    "ph_card".parse().unwrap()
143}
144
145impl Default for CardConfig {
146    fn default() -> Self {
147        serde_json::from_value(serde_json::json!({}))
148            .expect("expected all fields of CardConfig to have default values")
149    }
150}
151
152/// The different types of PubHubs cards
153#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, Copy, Default)]
154#[serde(rename_all = "snake_case")]
155pub enum CardType {
156    Demo,
157    #[default]
158    Real,
159}
160
161impl CardType {
162    pub fn credential(self) -> &'static str {
163        match self {
164            Self::Real => "pbdf.PubHubs.account",
165            Self::Demo => "irma-demo.PubHubs.account",
166        }
167    }
168
169    pub fn id(&self) -> &'static str {
170        "id"
171    }
172
173    pub fn date(self) -> &'static str {
174        match self {
175            Self::Real => "registrationDate",
176            Self::Demo => "registration_date",
177        }
178    }
179
180    pub fn source(self) -> &'static str {
181        match self {
182            Self::Real => "registrationSource",
183            Self::Demo => "registration_source",
184        }
185    }
186}
187
188impl App {
189    /// Gets the registration source to use when issuing a pubhubs card
190    pub(crate) fn registration_source<'a>(&'a self, yivi: &'a YiviCtx) -> &'a str {
191        if let Some(rs) = yivi.card_config.registration_source.as_ref() {
192            return rs.as_str();
193        }
194
195        self.phc_url.as_str()
196    }
197
198    /// Creates a yivi issuance request and pubhubs attribute for a PubHubs card
199    pub(crate) fn issue_card(
200        &self,
201        card_pseud_package: api::phc::user::CardPseudPackage,
202        comment: Option<String>,
203    ) -> api::Result<(yivi::ExtendedSessionRequest, attr::Attr)> {
204        let yivi = self.get_yivi()?;
205
206        let mut registration_date: String = card_pseud_package
207            .registration_date
208            .as_ref()
209            .map(api::NumericDate::date)
210            .unwrap_or_else(|| "?".to_string());
211
212        if let Some(comment) = comment {
213            registration_date = format!("{registration_date}, {comment}");
214        }
215
216        let card_pseud = card_pseud_package.card_pseud.to_string();
217
218        let credential = yivi::CredentialToBeIssued::new(
219            yivi.card_config
220                .card_type
221                .credential()
222                .parse()
223                .map_err(|err| {
224                    log::error!("failed to parse pubhubs card yivi credential: {err}");
225                    api::ErrorCode::InternalError
226                })?,
227        )
228        .valid_for(yivi.card_config.valid_for.now())
229        .attribute(
230            yivi.card_config.card_type.id().to_string(),
231            card_pseud.clone(),
232        )
233        .attribute(
234            yivi.card_config.card_type.source().to_string(),
235            self.registration_source(yivi).to_string(),
236        )
237        .attribute(
238            yivi.card_config.card_type.date().to_string(),
239            registration_date,
240        );
241
242        let esr = yivi::ExtendedSessionRequest::issuance(vec![credential]);
243
244        let Some(attr_type) = self.attr_type_from_handle(&yivi.card_config.card_attr_type) else {
245            log::error!(
246                "pubhubs card attribute type {} not found",
247                yivi.card_config.card_attr_type
248            );
249            return Err(api::ErrorCode::InternalError);
250        };
251
252        let attr = attr::Attr {
253            attr_type: attr_type.id,
254            value: card_pseud,
255            bannable: attr_type.bannable,
256            not_identifying: !attr_type.identifying,
257            not_addable: false,
258        };
259
260        Ok((esr, attr))
261    }
262
263    /// Implements [`api::auths::CardEP`].
264    pub async fn handle_card(
265        app: Rc<Self>,
266        req: web::Json<api::auths::CardReq>,
267    ) -> api::Result<api::auths::CardResp> {
268        let yivi = app.get_yivi()?;
269        let running_state = app.running_state_or_please_retry()?;
270
271        let api::auths::CardReq {
272            card_pseud_package: cpp_signed,
273            comment,
274        } = req.into_inner();
275
276        let card_pseudonym_package =
277            match cpp_signed.open(&*running_state.constellation.phc_jwt_key, None) {
278                Ok(cpp) => cpp,
279                Err(OpenError::OtherConstellation(..)) | Err(OpenError::InternalError) => {
280                    return Err(api::ErrorCode::InternalError);
281                }
282                Err(OpenError::OtherwiseInvalid) => {
283                    return Err(api::ErrorCode::BadRequest);
284                }
285                Err(OpenError::Expired) | Err(OpenError::InvalidSignature) => {
286                    return Ok(api::auths::CardResp::PleaseRetryWithNewCardPseud);
287                }
288            };
289
290        let (esr, attr) = app.issue_card(card_pseudonym_package, comment)?;
291
292        let issuance_request = esr.sign(&yivi.requestor_creds).map_err(|err| {
293            log::error!("failed to sign extended session request for issuance of card: {err:?}");
294            api::ErrorCode::InternalError
295        })?;
296
297        let attr = api::Signed::<attr::Attr>::new(
298            &running_state.attr_signing_key,
299            &attr,
300            app.auth_window,
301        )?;
302
303        Ok(api::auths::CardResp::Success {
304            attr,
305            issuance_request,
306            yivi_requestor_url: yivi.requestor_url.clone(),
307        })
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_card_valid_for() {
317        let cvf: CardValidFor = serde_json::from_value(serde_json::json!("3w")).unwrap();
318        assert_eq!(
319            cvf,
320            CardValidFor::Simple(core::time::Duration::from_hours(3 * 24 * 7))
321        );
322
323        assert_eq!(cvf.now(), core::time::Duration::from_hours(3 * 24 * 7));
324
325        let cvf: CardValidFor = serde_json::from_value(serde_json::json!([
326            { "starting_epoch": 521, "value": "4w" },  // epoch for 1980-01-01
327            { "starting_epoch": 1565, "value": "5w" } // epoch for 2000-01-01
328        ]))
329        .unwrap();
330
331        assert_eq!(cvf.now(), core::time::Duration::from_hours(5 * 24 * 7));
332        assert_eq!(
333            cvf.at(api::NumericDate::from(
334                humantime::parse_rfc3339_weak("1980-01-01 00:00:00").unwrap()
335            )),
336            core::time::Duration::from_hours(4 * 24 * 7)
337        );
338        assert_eq!(
339            cvf.at(api::NumericDate::from(
340                // start of epoch 512
341                humantime::parse_rfc3339_weak("1979-12-27T00:00:00").unwrap()
342            )),
343            core::time::Duration::from_hours(4 * 24 * 7)
344        );
345        assert_eq!(
346            cvf.at(api::NumericDate::from(
347                // the second before epoch 512 starts
348                humantime::parse_rfc3339_weak("1979-12-26T23:59:59").unwrap()
349            )),
350            core::time::Duration::from_hours(2 * 24 * 7)
351        );
352    }
353}