Skip to main content

web_eid/
auth.rs

1use base64::engine::general_purpose::STANDARD as BASE64;
2use base64::Engine;
3use esteid_cryptoki::IdCard;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256, Sha384, Sha512};
6use tokenkey::{EcCurve, Hash, KeyAlgorithm, SignScheme};
7
8use crate::error::{Result, WebEidError};
9
10/// The Web eID authentication token format identifier.
11pub const TOKEN_FORMAT: &str = "web-eid:1.0";
12
13/// A Web eID authentication token.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct AuthToken {
17    /// Base64-encoded DER of the user's authentication certificate.
18    pub unverified_certificate: String,
19    /// The signature algorithm.
20    pub algorithm: String,
21    /// Base64-encoded signature of the token.
22    pub signature: String,
23    /// The type identifier and version of the token format.
24    pub format: String,
25    /// URL identifying the application that issued the token.
26    pub app_version: String,
27}
28
29impl AuthToken {
30    /// The token in its JSON wire format.
31    pub fn to_json(&self) -> Result<String> {
32        Ok(serde_json::to_string(self)?)
33    }
34}
35
36/// Authenticate with the ID card: sign the origin and challenge nonce with
37/// the authentication key (PIN1) and produce a Web eID authentication token.
38///
39/// `origin` is the
40/// [ASCII serialization of the website origin](https://html.spec.whatwg.org/multipage/browsers.html#ascii-serialisation-of-an-origin)
41/// of the relying party.
42///
43/// The signed data is `hash(origin) || hash(challenge_nonce)`.
44pub fn authenticate(
45    card: &IdCard,
46    origin: &str,
47    challenge_nonce: &[u8],
48    pin1: &str,
49) -> Result<AuthToken> {
50    validate_origin(origin)?;
51
52    let KeyAlgorithm::Ec(curve) = card.auth.algorithm()?;
53    let (hash, algorithm) = curve_parameters(curve);
54
55    let mut data_to_sign = digest(hash, origin.as_bytes());
56    data_to_sign.extend_from_slice(&digest(hash, challenge_nonce));
57
58    let key = card.open_auth(pin1)?;
59    let signature = key.sign(SignScheme::Ecdsa(hash), &data_to_sign)?;
60
61    Ok(AuthToken {
62        unverified_certificate: BASE64.encode(card.auth_certificate_der()),
63        algorithm: algorithm.to_string(),
64        signature: BASE64.encode(signature),
65        format: TOKEN_FORMAT.to_string(),
66        app_version: app_version(),
67    })
68}
69
70fn curve_parameters(curve: EcCurve) -> (Hash, &'static str) {
71    match curve {
72        EcCurve::P256 => (Hash::Sha256, "ES256"),
73        EcCurve::P384 => (Hash::Sha384, "ES384"),
74        EcCurve::P521 => (Hash::Sha512, "ES512"),
75    }
76}
77
78fn digest(hash: Hash, data: &[u8]) -> Vec<u8> {
79    match hash {
80        Hash::Sha256 => Sha256::digest(data).to_vec(),
81        Hash::Sha384 => Sha384::digest(data).to_vec(),
82        Hash::Sha512 => Sha512::digest(data).to_vec(),
83    }
84}
85
86fn app_version() -> String {
87    format!(
88        "https://github.com/takakv/web-eid/releases/{}",
89        env!("CARGO_PKG_VERSION")
90    )
91}
92
93fn validate_origin(origin: &str) -> Result<()> {
94    let host = origin
95        .strip_prefix("https://")
96        .ok_or_else(|| WebEidError::InvalidOrigin(origin.to_string()))?;
97    if host.is_empty() || host.contains('/') {
98        return Err(WebEidError::InvalidOrigin(origin.to_string()));
99    }
100    Ok(())
101}