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
10pub const TOKEN_FORMAT: &str = "web-eid:1.0";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct AuthToken {
17 pub unverified_certificate: String,
19 pub algorithm: String,
21 pub signature: String,
23 pub format: String,
25 pub app_version: String,
27}
28
29impl AuthToken {
30 pub fn to_json(&self) -> Result<String> {
32 Ok(serde_json::to_string(self)?)
33 }
34}
35
36pub 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}