Compare commits

...

3 commits

Author SHA1 Message Date
puck 8b812da751 Handle non-authentication actions 2025-12-08 12:00:22 +00:00
puck 8f188a05cf Remove stray comments 2025-12-08 12:00:22 +00:00
puck 3636245aa7 Handle PIN failures 2025-12-08 12:00:22 +00:00
9 changed files with 148 additions and 118 deletions

21
Cargo.lock generated
View file

@ -1633,6 +1633,26 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@ -2201,6 +2221,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"thiserror",
"tokio",
"url",
]

View file

@ -20,3 +20,4 @@ url = "2.5.7"
gdk = { package = "gdk4", version = "0.10.3", features = ["v4_20"] }
async-channel = "2.5.0"
glib = { version = "0.21.5", features = ["v2_86"] }
thiserror = "2.0.17"

View file

@ -15,7 +15,7 @@ rustPlatform.buildRustPackage rec {
src = ./.;
cargoHash = "sha256-hrxH1Cxuf6oaVKuEDoB6W1qYdxlJ+dstU58ZO0NY+xg=";
cargoHash = "sha256-CO/9N4hDU1sh3gycE/FMZ3k1eDJR8krpBy6rCkKTfjo=";
nativeBuildInputs = [
pkg-config

View file

@ -64,7 +64,7 @@ pub struct PolyDataResponse {
pub result: String,
}
pub async fn wid_init(session_id: &str) -> ClientContext {
pub async fn wid_init(session_id: &str) -> Option<ClientContext> {
let client = reqwest::Client::new();
let init_req = client
.post("https://app.digid.nl/apps/wid/new")
@ -82,9 +82,10 @@ pub async fn wid_init(session_id: &str) -> ClientContext {
.await
.unwrap();
println!("{:?}", init_req);
if init_req.get("status").map(|f| f == "OK") != Some(true) {
return None;
}
let wid_session_id = init_req.get("session_id").unwrap().to_owned();
client
.post("https://app.digid.nl/apps/wid/confirm")
.json(&serde_json::json!({"app_session_id": session_id.to_owned() }))
@ -100,11 +101,19 @@ pub async fn wid_init(session_id: &str) -> ClientContext {
.json::<HashMap<String, String>>()
.await
.unwrap();
ClientContext {
let wid_session_id = init_req.get("session_id").unwrap().to_owned();
Some(ClientContext {
host: init_req.get("url").unwrap().to_owned(),
session: wid_session_id,
service: init_req.get("webservice").unwrap().to_owned(),
}
service: if let Some(v) = init_req.get("webservice") {
format!("log in to {}", v)
} else if let Some(v) = init_req.get("action") {
format!("perform '{}'", v)
} else {
"unknown action".to_owned()
},
})
}
pub struct ClientContext {

View file

@ -108,6 +108,7 @@ fn build_ui(
btn.set_sensitive(false);
let pass = password_field.text().to_string();
gtc_pipe.send_blocking(pipe::GUIToCard::PIN(pass)).unwrap();
password_field.set_text("");
}
));
@ -126,7 +127,7 @@ fn build_ui(
while let Ok(msg) = ctg_pipe.recv().await {
match msg {
pipe::CardToGUI::AuthenticationTarget { target } => {
info_label.set_text(&format!("Enter your PIN to log in to {}", target));
info_label.set_text(&format!("Enter your PIN to {}", target));
}
pipe::CardToGUI::WaitForCard => {

View file

@ -1,27 +1,4 @@
use crate::{Card, CommandChaining, OwnedCommandAPDU, SecureMessaging};
/**
* A card contains a master file.
* Each master file contains DFs, which can contain child DFs.
* EFs contain data.
*
* There may not be an MF but lke. yeah
*/
/*
PCA: A0000007885043 412D654D525444
EF.DIR is EF identifier 2F00 under dedicated file
EF.CardAccess (PACE) is EF identifier 011C under dedicated file
EID: A0000007885043 412D654D525444 *encrypted*
EF.DG14 is EF identifier 010E under dedicated file
(icao doc 9303-10)
EF.SOD is EF identifier 011D under dedicated file
(icao doc 9303-10)
EF.CVCA is EF identifier 011C under dedicated file
(icao doc 9303-11)
*/
#[derive(Copy, Clone, Debug)]
pub enum SelectFile<'a> {
@ -49,9 +26,6 @@ pub mod files {
pub const EF_SOD: SelectFile<'static> =
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1D]);
// static 36 bytes?
// same ID as EF.CardAccess???
pub const EF_CVCA: SelectFile<'static> =
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1C]);
}

View file

@ -105,7 +105,11 @@ async fn run_auth(
service: String::from("UI Test"),
}
} else {
let ctx = digid_api::wid_init(&session_id).await;
let Some(ctx) = digid_api::wid_init(&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
};
@ -157,74 +161,88 @@ async fn run_auth(
break crad;
};
// 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?;
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?;
// 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();
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 status = pace::set_authentication_template(
&mut crad,
ef_cardaccess.get(0).unwrap().protocol,
pace::PasswordType::PIN,
)
.await?;
let (msg, can_continue) = match status {
pace::PACEStatus::Okay => (None, true),
pace::PACEStatus::TriesLeft(n) => (Some(format!("{} tries left", n)), true),
pace::PACEStatus::Error(unk) => (Some(format!("Unknown error {:04x}", unk)), false),
pace::PACEStatus::PasswordSuspended => (Some("PIN suspended. Use app.".to_string()), false),
pace::PACEStatus::PasswordBlocked => (Some("PIN blocked. Use app.".to_string()), false),
};
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 {
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: msg.unwrap(),
message: String::from("Negotiating with the card..."),
})
.await;
}
let GUIToCard::PIN(pin) = gtc_pipe.recv().await.unwrap();
ctg_pipe
.send(pipe::CardToGUI::ProcessingMessage {
message: String::from("Negotiating with the card..."),
})
.await;
let creds = pace::authenticate_pin(
&mut crad,
pin.as_bytes(),
ef_cardaccess.get(0).unwrap().protocol,
)
.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;

View file

@ -317,13 +317,19 @@ pub enum PasswordType {
PUK = 0x04,
}
#[derive(Debug)]
#[derive(thiserror::Error, Debug)]
pub enum PACEStatus {
Okay,
#[error("transmit error: {0:04x}")]
Error(u16),
#[error("{0} tries left")]
TriesLeft(u8),
#[error("PIN suspended")]
PasswordSuspended,
#[error("PIN blocked")]
PasswordBlocked,
#[error("Card error: {0}")]
CardError(#[from] std::io::Error),
}
fn make_set_authentication_template_apdu(
@ -351,7 +357,7 @@ pub async fn set_authentication_template(
card: &mut impl Card,
cryptographic_mechanism: ObjectIdentifier,
password: PasswordType,
) -> std::io::Result<PACEStatus> {
) -> Result<(), PACEStatus> {
let d = card
.transmit(make_set_authentication_template_apdu(
cryptographic_mechanism,
@ -359,21 +365,21 @@ pub async fn set_authentication_template(
))
.await?;
Ok(match d.status {
0x9000 => PACEStatus::Okay,
v if v & 0xFFF0 == 0x63C0 => PACEStatus::TriesLeft((v as u8) & 0xF),
0x63C1 => PACEStatus::PasswordSuspended,
0x63C0 => PACEStatus::PasswordBlocked,
match d.status {
0x9000 => Ok(()),
v if v & 0xFFF0 == 0x63C0 => Err(PACEStatus::TriesLeft((v as u8) & 0xF)),
0x63C1 => Err(PACEStatus::PasswordSuspended),
0x63C0 => Err(PACEStatus::PasswordBlocked),
v => PACEStatus::Error(v),
})
v => Err(PACEStatus::Error(v)),
}
}
pub async fn step_general_authenticate(
card: &mut impl Card,
chained: bool,
make_data: impl FnOnce(&mut Vec<u8>),
) -> std::io::Result<HashMap<u8, Vec<u8>>> {
) -> Result<HashMap<u8, Vec<u8>>, PACEStatus> {
let mut buf = Vec::new();
make_data(&mut buf);
prepend_do(&mut buf, 0x7c);
@ -398,7 +404,16 @@ pub async fn step_general_authenticate(
})
.await?;
if !res.data.starts_with(&[0x7c]) || res.status != 0x9000 {
match res.status {
0x9000 => (),
v if v & 0xFFF0 == 0x63C0 => return Err(PACEStatus::TriesLeft((v as u8) & 0xF)),
0x63C1 => return Err(PACEStatus::PasswordSuspended),
0x63C0 => return Err(PACEStatus::PasswordBlocked),
v => return Err(PACEStatus::Error(v)),
}
if !res.data.starts_with(&[0x7c]) {
return Ok(HashMap::new());
}
@ -425,7 +440,7 @@ pub async fn authenticate_pin(
card: &mut impl Card,
pin: &[u8],
cryptographic_mechanism: ObjectIdentifier,
) -> std::io::Result<PACECredentials> {
) -> Result<PACECredentials, PACEStatus> {
// Step one: Get the encrypted nonce
let mut data = step_general_authenticate(card, true, |_| {}).await?;

View file

@ -42,16 +42,7 @@ impl Card for PCSCCard {
apdu_buf.push(apdu.expected_length.unwrap_or_default() as u8);
}
let ret_len = self
.card
.transmit(&apdu_buf, &mut self.buf)
.map_err(|f| std::io::Error::new(std::io::ErrorKind::BrokenPipe, f))?
.len();
let data = self.buf[..ret_len - 2].to_vec();
let sw = (self.buf[ret_len - 2] as u16) << 8 | (self.buf[ret_len - 1] as u16);
Ok(ResultAPDU { data, status: sw })
self.transmit_raw(&apdu_buf).await
}
async fn transmit_raw(&mut self, apdu_buf: &[u8]) -> std::io::Result<crate::ResultAPDU> {
let ret_len = self