Skip to main content

esteid_cryptoki/
card.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use tokenkey::{discover, DiscoveredKey, Discovery, KeyClass, TokenKey};
5
6use crate::error::Result;
7use crate::filter::{is_esteid_issued, modules};
8use crate::EstEidError;
9
10/// An Estonian ID card.
11pub struct IdCard {
12    /// The Authentication key (PIN1).
13    pub auth: DiscoveredKey,
14    /// The Signing key (PIN2).
15    pub sign: DiscoveredKey,
16}
17
18impl IdCard {
19    /// All discovered connected Estonian ID cards.
20    pub fn list() -> Result<Vec<IdCard>> {
21        Ok(cards_from_pairs(pair_cards(discover(&modules()))))
22    }
23
24    /// Find a specific ID card.
25    pub fn find() -> Result<IdCard> {
26        find_from_pairs(pair_cards(discover(&modules())))
27    }
28
29    /// The card's document number.
30    pub fn document_number(&self) -> &str {
31        &self.auth.token_serial
32    }
33
34    /// DER of the Authentication certificate.
35    pub fn auth_certificate_der(&self) -> &[u8] {
36        &self.auth.cert_der
37    }
38
39    /// DER of the Signing certificate.
40    pub fn signing_certificate_der(&self) -> &[u8] {
41        &self.sign.cert_der
42    }
43
44    /// Open the Authentication key for signing.
45    /// Requires PIN1.
46    pub fn open_auth(&self, pin1: &str) -> Result<TokenKey> {
47        Ok(TokenKey::open_discovered(&self.auth, pin1)?)
48    }
49
50    /// Open the Signing key for qualified signing.
51    /// Requires PIN2.
52    pub fn open_signing(&self, pin2: &str) -> Result<TokenKey> {
53        Ok(TokenKey::open_discovered(&self.sign, pin2)?)
54    }
55}
56
57enum CardOutcome {
58    /// Both key types present.
59    Complete(Box<IdCard>),
60    /// Only one key type present.
61    Incomplete {
62        token_label: String,
63        token_serial: String,
64        missing: &'static str,
65    },
66}
67
68fn pair_cards(discovery: Discovery) -> Vec<CardOutcome> {
69    // OpenSC exposes a card's auth and signing keys as separate slots.
70    // We can group by serial since the serial is the same for both.
71    let mut groups: BTreeMap<(PathBuf, String), (Option<DiscoveredKey>, Option<DiscoveredKey>)> =
72        BTreeMap::new();
73
74    for key in discovery.keys {
75        if !matches!(key.usage, KeyClass::Authentication | KeyClass::Signing) {
76            continue;
77        }
78        if !is_esteid_issued(&key.cert_der) {
79            continue;
80        }
81        let token = (key.module.clone(), key.token_serial.clone());
82        let entry = groups.entry(token).or_insert((None, None));
83        match key.usage {
84            KeyClass::Authentication => entry.0 = Some(key),
85            KeyClass::Signing => entry.1 = Some(key),
86            KeyClass::Other => unreachable!("filtered above"),
87        }
88    }
89
90    groups
91        .into_values()
92        .map(|(auth, signing)| match (auth, signing) {
93            (Some(auth), Some(signing)) => CardOutcome::Complete(Box::new(IdCard {
94                auth,
95                sign: signing,
96            })),
97            (Some(auth), None) => CardOutcome::Incomplete {
98                token_label: auth.token_label,
99                token_serial: auth.token_serial,
100                missing: "a signing certificate",
101            },
102            (None, Some(signing)) => CardOutcome::Incomplete {
103                token_label: signing.token_label,
104                token_serial: signing.token_serial,
105                missing: "an authentication certificate",
106            },
107            (None, None) => unreachable!("a group is only created when a key is inserted"),
108        })
109        .collect()
110}
111
112fn cards_from_pairs(outcomes: Vec<CardOutcome>) -> Vec<IdCard> {
113    outcomes
114        .into_iter()
115        .filter_map(|outcome| match outcome {
116            CardOutcome::Complete(card) => Some(*card),
117            CardOutcome::Incomplete { .. } => None,
118        })
119        .collect()
120}
121
122fn find_from_pairs(outcomes: Vec<CardOutcome>) -> Result<IdCard> {
123    let mut complete = Vec::new();
124    let mut incomplete = Vec::new();
125    for outcome in outcomes {
126        match outcome {
127            CardOutcome::Complete(card) => complete.push(*card),
128            CardOutcome::Incomplete {
129                token_label,
130                token_serial,
131                missing,
132            } => incomplete.push((token_label, token_serial, missing)),
133        }
134    }
135
136    match (complete.len(), incomplete.len()) {
137        (0, 0) => Err(EstEidError::NoCard),
138        (1, 0) => Ok(complete.into_iter().next().expect("len checked above")),
139        (0, 1) => {
140            let (_, _, missing) = incomplete.into_iter().next().expect("len checked above");
141            Err(EstEidError::IncompleteCard(missing.to_string()))
142        }
143        _ => {
144            let mut seen: Vec<String> = complete
145                .iter()
146                .map(|card| describe(&card.auth.token_label, &card.auth.token_serial))
147                .collect();
148            seen.extend(
149                incomplete
150                    .into_iter()
151                    .map(|(label, serial, _)| describe(&label, &serial)),
152            );
153            Err(EstEidError::MultipleCards(seen))
154        }
155    }
156}
157
158fn describe(token_label: &str, token_serial: &str) -> String {
159    format!("{token_label} ({token_serial})")
160}