use std::{collections::HashMap, env::args, thread, time::Duration}; use der::{Any, Decode, asn1::SetOfVec, oid::ObjectIdentifier}; use openssl::{bn::BigNumContext, ec::PointConversionForm, pkey::PKey}; use tokio::runtime::Runtime; use url::Url; use crate::{ pace::{append_do, prepend_do}, pcsc_card::{PCSCCard, PCSCCardFinder}, pipe::GUIToCard, }; mod digid_api; mod gui; mod iso7816; mod pace; mod pcsc_card; mod pipe; #[derive(Clone, Debug)] pub struct ResultAPDU { pub data: Vec, pub status: u16, } #[derive(Clone, Debug)] pub struct OwnedCommandAPDU { pub class: Class, pub instruction: u8, pub parameter: [u8; 2], pub command: Vec, pub expected_length: Option, } #[derive(Copy, Clone, Debug)] pub enum CommandChaining { LastOrOnly = 0, NotLast = 1, } #[derive(Copy, Clone, Debug)] pub enum SecureMessaging { None = 0b00, Proprietary = 0b01, StandardNoHeader = 0b10, StandardHeaderAuthenticated = 0b11, } #[derive(Copy, Clone, Debug)] pub enum Class { Proprietary(u8), Standard { command_chaining: CommandChaining, secure_messaging: SecureMessaging, channel: u8, }, } impl Class { pub fn encode(&self) -> Option { match *self { Class::Proprietary(n) if n < 0x80 => Some(0x80 | n), Class::Standard { command_chaining, secure_messaging, channel, } if channel < 4 => { Some(((command_chaining as u8) << 4) | ((secure_messaging as u8) << 2) | channel) } Class::Standard { command_chaining, secure_messaging: secure_messaging @ (SecureMessaging::None | SecureMessaging::StandardNoHeader), channel, } if channel < 20 => Some( 0x40 | ((command_chaining as u8) << 4) | ((secure_messaging as u8) << 4) | channel, ), _ => None, } } } pub trait Card { fn transmit( &mut self, apdu: OwnedCommandAPDU, ) -> impl Future> + Send; fn transmit_raw( &mut self, apdu_buf: &[u8], ) -> impl Future> + Send; } async fn run_auth( host: String, session_id: String, ctg_pipe: async_channel::Sender, gtc_pipe: async_channel::Receiver, ) -> std::io::Result<()> { let ctx = if session_id == "test" { digid_api::ClientContext { host: String::from("http://localhost"), session: String::from("test"), service: String::from("UI Test"), } } else { let Some(ctx) = digid_api::wid_init(&host, &session_id).await else { ctg_pipe.send(pipe::CardToGUI::ProcessingMessage { message: "Failed to initialize DigiD session.".to_owned() }).await; return Ok(()); }; ctx.start().await; ctx }; ctg_pipe .send(pipe::CardToGUI::AuthenticationTarget { target: ctx.service.clone(), }) .await; ctg_pipe.send(pipe::CardToGUI::WaitForCard).await; let mut finder = PCSCCardFinder::new(); let mut crad = loop { let mut crad = finder.find_valid().await; // Select MF iso7816::select( &mut crad, 0, iso7816::SelectFile::File(&[]), iso7816::SelectOccurrence::First, ) .await?; iso7816::select( &mut crad, 0, iso7816::files::EF_DIR, iso7816::SelectOccurrence::First, ) .await?; let ef_dir = iso7816::read_binary(&mut crad, 0).await?; if ef_dir.is_none() || !ef_dir.unwrap().windows(14).any(|w| { w == [ 0xA0, 0x00, 0x00, 0x07, 0x88, 0x50, 0x43, 0x41, 0x2D, 0x65, 0x4D, 0x52, 0x54, 0x44, ] }) { ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Card is not eID"), }) .await; ctg_pipe.send(pipe::CardToGUI::WaitForCard).await; continue; } break crad; }; let creds = loop { // Select the PCA application iso7816::select( &mut crad, 0, iso7816::SelectFile::DedicatedFileName(&[ 0xA0, 0x00, 0x00, 0x07, 0x88, 0x50, 0x43, 0x41, 0x2D, 0x65, 0x4D, 0x52, 0x54, 0x44, ]), iso7816::SelectOccurrence::First, ) .await?; // Select _its_ MF (what?) iso7816::select( &mut crad, 0, iso7816::SelectFile::File(&[]), iso7816::SelectOccurrence::First, ) .await?; iso7816::select( &mut crad, 0, iso7816::files::EF_CARDACCESS, iso7816::SelectOccurrence::First, ) .await?; let ef_cardaccess_bytes = iso7816::read_binary(&mut crad, 0).await?.unwrap(); let ef_cardaccess = pace::SecurityInfos::from_der(&ef_cardaccess_bytes).unwrap(); let (msg, can_continue) = match pace::set_authentication_template( &mut crad, ef_cardaccess.get(0).unwrap().protocol, pace::PasswordType::PIN, ) .await { Ok(()) => (None, true), Err(pace::PACEStatus::CardError(e)) => return Err(e), Err(pace::PACEStatus::TriesLeft(n)) => (Some(format!("{} tries left", n)), true), Err(pace::PACEStatus::Error(unk)) => { (Some(format!("Unknown error {:04x}", unk)), false) } Err(pace::PACEStatus::PasswordSuspended) => { (Some("PIN suspended. Use app.".to_string()), false) } Err(pace::PACEStatus::PasswordBlocked) => { (Some("PIN blocked. Use app.".to_string()), false) } }; if can_continue { ctg_pipe .send(pipe::CardToGUI::ReadyForPIN { message: msg }) .await; } else { ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: msg.unwrap(), }) .await; } let GUIToCard::PIN(pin) = gtc_pipe.recv().await.unwrap(); ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Negotiating with the card..."), }) .await; match pace::authenticate_pin( &mut crad, pin.as_bytes(), ef_cardaccess.get(0).unwrap().protocol, ) .await { Ok(creds) => break creds, Err(pace::PACEStatus::CardError(n)) => return Err(n), _ => (), } }; let apdus; { let mut enc_crad = pace::EncryptedCardWrapper::new(&mut crad, creds.clone()); // Select the PCA again iso7816::select( &mut enc_crad, 0, iso7816::SelectFile::DedicatedFileName(&[ 0xA0, 0x00, 0x00, 0x07, 0x88, 0x50, 0x43, 0x41, 0x2D, 0x65, 0x4D, 0x52, 0x54, 0x44, ]), iso7816::SelectOccurrence::First, ) .await?; ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Reading EF.DG14..."), }) .await; iso7816::select( &mut enc_crad, 0, iso7816::files::EF_DG14, iso7816::SelectOccurrence::First, ) .await?; let dg14 = iso7816::read_binary(&mut enc_crad, 0).await?.unwrap(); ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Reading EF.SOD..."), }) .await; iso7816::select( &mut enc_crad, 0, iso7816::files::EF_SOD, iso7816::SelectOccurrence::First, ) .await?; let ef_sod = iso7816::read_binary(&mut enc_crad, 0).await?.unwrap(); ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Reading EF.CVCA..."), }) .await; iso7816::select( &mut enc_crad, 0, iso7816::files::EF_CVCA, iso7816::SelectOccurrence::First, ) .await?; let ef_cvca = iso7816::read_binary(&mut enc_crad, 0).await?.unwrap(); ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Provided files for EAC..."), }) .await; if ctx.session == "test" { ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Test done."), }) .await; tokio::time::sleep(Duration::from_secs(2)).await; ctg_pipe.send(pipe::CardToGUI::Done).await; return Ok(()); } let (apdus_, remote_ephemeral_pkey_bytes) = ctx .prepare_eac(&ef_cvca, &dg14, &ef_sod, &creds.card_ephemeral_key) .await; apdus = apdus_; let barely_parsed_dg14_one = Any::from_der(&dg14).unwrap(); let barely_parsed_dg14_contents = SetOfVec::>::from_der(barely_parsed_dg14_one.value()).unwrap(); let ca_info_oid = ObjectIdentifier::new_unwrap("0.4.0.127.0.7.2.2.3"); let mut ca_info = None; for item in barely_parsed_dg14_contents.into_vec() { let oid = item .first() .unwrap() .decode_as::() .unwrap(); if oid.as_bytes().starts_with(ca_info_oid.as_bytes()) { ca_info = Some((oid, item.get(2).map(|f| f.value().to_vec()))); break; } } let ca_info = ca_info.unwrap(); let remote_ephemeral_key_pkey = PKey::public_key_from_der(&remote_ephemeral_pkey_bytes).unwrap(); let remote_ephemeral_key = remote_ephemeral_key_pkey.ec_key().unwrap(); let remote_ephemeral_key_group = remote_ephemeral_key.group(); let remote_ephemeral_key_point = remote_ephemeral_key.public_key(); let mut bn_ctx = BigNumContext::new().unwrap(); let mut remote_ephemeral_key_bytes = remote_ephemeral_key_point .to_bytes( remote_ephemeral_key_group, PointConversionForm::UNCOMPRESSED, &mut bn_ctx, ) .unwrap(); // Set up the first part of the terminal authentication: // send a SET AT with reference id-TA-ECDSA-SHA-256. let mut set_at_params = ca_info.0.as_bytes().to_vec(); prepend_do(&mut set_at_params, 0x80); if let Some(v) = ca_info.1 { append_do(&mut set_at_params, 0x84, &v); } let new_apdu = OwnedCommandAPDU { class: Class::Standard { command_chaining: CommandChaining::LastOrOnly, secure_messaging: SecureMessaging::None, channel: 0, }, instruction: 0x22, parameter: [0x41, 0xA4], command: set_at_params, expected_length: Some(0), }; let resp = enc_crad.transmit(new_apdu).await?; assert_eq!(resp.status, 0x9000); // Do a GENERAL AUTHENTICATE with all-0 parameters. // This _seems_ to swap the ephemeral key we've been using to // communicate with the card with that of the terminal's, and as side // effect resets the session. Once this is transmitted, we can no // longer communicate with the card in this session. prepend_do(&mut remote_ephemeral_key_bytes, 0x80); prepend_do(&mut remote_ephemeral_key_bytes, 0x7c); let new_apdu = OwnedCommandAPDU { class: Class::Standard { command_chaining: CommandChaining::LastOrOnly, secure_messaging: SecureMessaging::None, channel: 0, }, instruction: 0x86, parameter: [0x00, 0x00], command: remote_ephemeral_key_bytes, expected_length: Some(0x16), }; let resp = enc_crad.transmit(new_apdu).await?; assert_eq!(resp.status, 0x9000); } // The APDUs sent by the terminal are to be sent now. // Because there's not a lot of documentation, here's what seems to be // happening: // // 1. The card-verifiable certificate chain between the one in EF.CVCA and // the certificate used by the terminal to authenticate are transmitted and // verified (SET DST, plus VERIFY CERTIFICATE). // // 2. A SET AT is issued with the terminal's certificate's subject. // 3. A GET CHALLENGE is issued to the card, with the response being // shuttled back to the terminal. // // This part follows standard BSI TR-03110, if you want to take a look. ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: String::from("Running server-sent APDUs... (0/?)"), }) .await; let mut counter = -1isize; let mut last_response = ResultAPDU { data: Vec::new(), status: 0, }; let apdu_count = apdus.len(); for apdu in apdus { counter += 1; ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: format!("Running server-sent APDUs... ({}/{})", counter, apdu_count), }) .await; last_response = crad.transmit_raw(&apdu).await?; if last_response.status != 0x9000 { break; } } last_response .data .extend_from_slice(&last_response.status.to_be_bytes()); let apdus = ctx.prepare_pca(counter, &last_response.data).await; // 1. The terminal uses the challenge, plus previously-cached information, // to sign the challenge; this signature is sent back as a EXTERNAL // AUTHENTICATE. The terminal is now authenticated. // // 2. The terminal transmits a SET AT, with an OID of { id-BSNk-scheme-nl 9 // 3 3 } (assuming a PIP). This is immediately followed by a GENERAL // AUTHENTICATE, which has no parameters (TODO: check) and returns the // polymorphic identity, as a CardPolymorph. // // A CardPolymorph is: // - 0x80 Point0 (ECPoint) // - 0x81 Point1 (ECPoint, optional) // - 0x82 Point2 (ECPoint, optional) // - 0x85 Scheme version (INTEGER) // - 0x86 Scheme key version (INTEGER) // - 0x87 Creator (BCD string) // - 0x88 Recipient (BCD String) // - 0x89 Recipient key set version (INTEGER) // - 0x8a Type (INTEGER) // - 0x8b Sequence number (BCD string) let apdu_count = apdus.len() as isize + counter; for apdu in apdus { counter += 1; ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: format!("Running server-sent APDUs... ({}/{})", counter, apdu_count), }) .await; last_response = crad.transmit_raw(&apdu).await?; if last_response.status != 0x9000 { break; } } last_response .data .extend_from_slice(&last_response.status.to_be_bytes()); let result = ctx.get_polymorphic_data(counter, &last_response.data).await; ctg_pipe .send(pipe::CardToGUI::ProcessingMessage { message: format!("Server said: {}", result), }) .await; tokio::time::sleep(Duration::from_secs(3)).await; ctg_pipe.send(pipe::CardToGUI::Done).await; Ok(()) } fn main() { let (ctg_pipe_s, ctg_pipe_r) = async_channel::unbounded(); let (gtc_pipe_s, gtc_pipe_r) = async_channel::unbounded(); let s = args().nth(1).unwrap(); let mut parsed_url = url::form_urlencoded::parse(s.split(':').last().unwrap().as_bytes()).into_owned().collect::>(); let rt = Runtime::new().unwrap(); rt.spawn(async move { run_auth(parsed_url.remove("host").unwrap_or_else(|| String::from("test")), parsed_url.remove("app_session_id").unwrap_or_else(|| String::from("test")), ctg_pipe_s, gtc_pipe_r).await.unwrap() }); gui::run_gui(ctg_pipe_r, gtc_pipe_s); }