Compare commits
3 commits
c9e96b3260
...
2662917b29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2662917b29 | ||
|
|
428574a711 | ||
|
|
bb8d954510 |
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -1633,6 +1633,26 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
@ -2201,6 +2221,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,4 @@ url = "2.5.7"
|
||||||
gdk = { package = "gdk4", version = "0.10.3", features = ["v4_20"] }
|
gdk = { package = "gdk4", version = "0.10.3", features = ["v4_20"] }
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
glib = { version = "0.21.5", features = ["v2_86"] }
|
glib = { version = "0.21.5", features = ["v2_86"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ fn build_ui(
|
||||||
btn.set_sensitive(false);
|
btn.set_sensitive(false);
|
||||||
let pass = password_field.text().to_string();
|
let pass = password_field.text().to_string();
|
||||||
gtc_pipe.send_blocking(pipe::GUIToCard::PIN(pass)).unwrap();
|
gtc_pipe.send_blocking(pipe::GUIToCard::PIN(pass)).unwrap();
|
||||||
|
password_field.set_text("");
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,4 @@
|
||||||
use crate::{Card, CommandChaining, OwnedCommandAPDU, SecureMessaging};
|
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)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum SelectFile<'a> {
|
pub enum SelectFile<'a> {
|
||||||
|
|
@ -49,9 +26,6 @@ pub mod files {
|
||||||
|
|
||||||
pub const EF_SOD: SelectFile<'static> =
|
pub const EF_SOD: SelectFile<'static> =
|
||||||
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1D]);
|
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1D]);
|
||||||
|
|
||||||
// static 36 bytes?
|
|
||||||
// same ID as EF.CardAccess???
|
|
||||||
pub const EF_CVCA: SelectFile<'static> =
|
pub const EF_CVCA: SelectFile<'static> =
|
||||||
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1C]);
|
SelectFile::ElementaryFileUnderDedicatedFile(&[0x01, 0x1C]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
src/main.rs
177
src/main.rs
|
|
@ -157,74 +157,88 @@ async fn run_auth(
|
||||||
break crad;
|
break crad;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Select the PCA application
|
let creds = loop {
|
||||||
iso7816::select(
|
// Select the PCA application
|
||||||
&mut crad,
|
iso7816::select(
|
||||||
0,
|
&mut crad,
|
||||||
iso7816::SelectFile::DedicatedFileName(&[
|
0,
|
||||||
0xA0, 0x00, 0x00, 0x07, 0x88, 0x50, 0x43, 0x41, 0x2D, 0x65, 0x4D, 0x52, 0x54, 0x44,
|
iso7816::SelectFile::DedicatedFileName(&[
|
||||||
]),
|
0xA0, 0x00, 0x00, 0x07, 0x88, 0x50, 0x43, 0x41, 0x2D, 0x65, 0x4D, 0x52, 0x54, 0x44,
|
||||||
iso7816::SelectOccurrence::First,
|
]),
|
||||||
)
|
iso7816::SelectOccurrence::First,
|
||||||
.await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Select _its_ MF (what?)
|
// Select _its_ MF (what?)
|
||||||
iso7816::select(
|
iso7816::select(
|
||||||
&mut crad,
|
&mut crad,
|
||||||
0,
|
0,
|
||||||
iso7816::SelectFile::File(&[]),
|
iso7816::SelectFile::File(&[]),
|
||||||
iso7816::SelectOccurrence::First,
|
iso7816::SelectOccurrence::First,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
iso7816::select(
|
iso7816::select(
|
||||||
&mut crad,
|
&mut crad,
|
||||||
0,
|
0,
|
||||||
iso7816::files::EF_CARDACCESS,
|
iso7816::files::EF_CARDACCESS,
|
||||||
iso7816::SelectOccurrence::First,
|
iso7816::SelectOccurrence::First,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let ef_cardaccess_bytes = iso7816::read_binary(&mut crad, 0).await?.unwrap();
|
let ef_cardaccess_bytes = iso7816::read_binary(&mut crad, 0).await?.unwrap();
|
||||||
let ef_cardaccess = pace::SecurityInfos::from_der(&ef_cardaccess_bytes).unwrap();
|
let ef_cardaccess = pace::SecurityInfos::from_der(&ef_cardaccess_bytes).unwrap();
|
||||||
|
|
||||||
let status = pace::set_authentication_template(
|
let (msg, can_continue) = match pace::set_authentication_template(
|
||||||
&mut crad,
|
&mut crad,
|
||||||
ef_cardaccess.get(0).unwrap().protocol,
|
ef_cardaccess.get(0).unwrap().protocol,
|
||||||
pace::PasswordType::PIN,
|
pace::PasswordType::PIN,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
let (msg, can_continue) = match status {
|
{
|
||||||
pace::PACEStatus::Okay => (None, true),
|
Ok(()) => (None, true),
|
||||||
pace::PACEStatus::TriesLeft(n) => (Some(format!("{} tries left", n)), true),
|
Err(pace::PACEStatus::CardError(e)) => return Err(e),
|
||||||
pace::PACEStatus::Error(unk) => (Some(format!("Unknown error {:04x}", unk)), false),
|
Err(pace::PACEStatus::TriesLeft(n)) => (Some(format!("{} tries left", n)), true),
|
||||||
pace::PACEStatus::PasswordSuspended => (Some("PIN suspended. Use app.".to_string()), false),
|
Err(pace::PACEStatus::Error(unk)) => {
|
||||||
pace::PACEStatus::PasswordBlocked => (Some("PIN blocked. Use app.".to_string()), false),
|
(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 {
|
if can_continue {
|
||||||
ctg_pipe
|
ctg_pipe
|
||||||
.send(pipe::CardToGUI::ReadyForPIN { message: msg })
|
.send(pipe::CardToGUI::ReadyForPIN { message: msg })
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
|
ctg_pipe
|
||||||
|
.send(pipe::CardToGUI::ProcessingMessage {
|
||||||
|
message: msg.unwrap(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let GUIToCard::PIN(pin) = gtc_pipe.recv().await.unwrap();
|
||||||
ctg_pipe
|
ctg_pipe
|
||||||
.send(pipe::CardToGUI::ProcessingMessage {
|
.send(pipe::CardToGUI::ProcessingMessage {
|
||||||
message: msg.unwrap(),
|
message: String::from("Negotiating with the card..."),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
match pace::authenticate_pin(
|
||||||
|
&mut crad,
|
||||||
let GUIToCard::PIN(pin) = gtc_pipe.recv().await.unwrap();
|
pin.as_bytes(),
|
||||||
ctg_pipe
|
ef_cardaccess.get(0).unwrap().protocol,
|
||||||
.send(pipe::CardToGUI::ProcessingMessage {
|
)
|
||||||
message: String::from("Negotiating with the card..."),
|
.await
|
||||||
})
|
{
|
||||||
.await;
|
Ok(creds) => break creds,
|
||||||
let creds = pace::authenticate_pin(
|
Err(pace::PACEStatus::CardError(n)) => return Err(n),
|
||||||
&mut crad,
|
_ => (),
|
||||||
pin.as_bytes(),
|
}
|
||||||
ef_cardaccess.get(0).unwrap().protocol,
|
};
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let apdus;
|
let apdus;
|
||||||
|
|
||||||
|
|
@ -337,8 +351,8 @@ async fn run_auth(
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// We send a new SET AT along with a GENERAL AUTHENTICATE for internal/restricted identification.
|
// Set up the first part of the terminal authentication:
|
||||||
// This has to be encrypted, for Reasons.
|
// send a SET AT with reference id-TA-ECDSA-SHA-256.
|
||||||
let mut set_at_params = ca_info.0.as_bytes().to_vec();
|
let mut set_at_params = ca_info.0.as_bytes().to_vec();
|
||||||
prepend_do(&mut set_at_params, 0x80);
|
prepend_do(&mut set_at_params, 0x80);
|
||||||
if let Some(v) = ca_info.1 {
|
if let Some(v) = ca_info.1 {
|
||||||
|
|
@ -359,6 +373,11 @@ async fn run_auth(
|
||||||
let resp = enc_crad.transmit(new_apdu).await?;
|
let resp = enc_crad.transmit(new_apdu).await?;
|
||||||
assert_eq!(resp.status, 0x9000);
|
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, 0x80);
|
||||||
prepend_do(&mut remote_ephemeral_key_bytes, 0x7c);
|
prepend_do(&mut remote_ephemeral_key_bytes, 0x7c);
|
||||||
let new_apdu = OwnedCommandAPDU {
|
let new_apdu = OwnedCommandAPDU {
|
||||||
|
|
@ -376,6 +395,19 @@ async fn run_auth(
|
||||||
assert_eq!(resp.status, 0x9000);
|
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
|
ctg_pipe
|
||||||
.send(pipe::CardToGUI::ProcessingMessage {
|
.send(pipe::CardToGUI::ProcessingMessage {
|
||||||
message: String::from("Running server-sent APDUs... (0/?)"),
|
message: String::from("Running server-sent APDUs... (0/?)"),
|
||||||
|
|
@ -405,6 +437,27 @@ async fn run_auth(
|
||||||
.extend_from_slice(&last_response.status.to_be_bytes());
|
.extend_from_slice(&last_response.status.to_be_bytes());
|
||||||
|
|
||||||
let apdus = ctx.prepare_pca(counter, &last_response.data).await;
|
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;
|
let apdu_count = apdus.len() as isize + counter;
|
||||||
for apdu in apdus {
|
for apdu in apdus {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
|
|
|
||||||
41
src/pace.rs
41
src/pace.rs
|
|
@ -317,13 +317,19 @@ pub enum PasswordType {
|
||||||
PUK = 0x04,
|
PUK = 0x04,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum PACEStatus {
|
pub enum PACEStatus {
|
||||||
Okay,
|
#[error("transmit error: {0:04x}")]
|
||||||
Error(u16),
|
Error(u16),
|
||||||
|
#[error("{0} tries left")]
|
||||||
TriesLeft(u8),
|
TriesLeft(u8),
|
||||||
|
#[error("PIN suspended")]
|
||||||
PasswordSuspended,
|
PasswordSuspended,
|
||||||
|
#[error("PIN blocked")]
|
||||||
PasswordBlocked,
|
PasswordBlocked,
|
||||||
|
|
||||||
|
#[error("Card error: {0}")]
|
||||||
|
CardError(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_set_authentication_template_apdu(
|
fn make_set_authentication_template_apdu(
|
||||||
|
|
@ -351,7 +357,7 @@ pub async fn set_authentication_template(
|
||||||
card: &mut impl Card,
|
card: &mut impl Card,
|
||||||
cryptographic_mechanism: ObjectIdentifier,
|
cryptographic_mechanism: ObjectIdentifier,
|
||||||
password: PasswordType,
|
password: PasswordType,
|
||||||
) -> std::io::Result<PACEStatus> {
|
) -> Result<(), PACEStatus> {
|
||||||
let d = card
|
let d = card
|
||||||
.transmit(make_set_authentication_template_apdu(
|
.transmit(make_set_authentication_template_apdu(
|
||||||
cryptographic_mechanism,
|
cryptographic_mechanism,
|
||||||
|
|
@ -359,21 +365,21 @@ pub async fn set_authentication_template(
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(match d.status {
|
match d.status {
|
||||||
0x9000 => PACEStatus::Okay,
|
0x9000 => Ok(()),
|
||||||
v if v & 0xFFF0 == 0x63C0 => PACEStatus::TriesLeft((v as u8) & 0xF),
|
v if v & 0xFFF0 == 0x63C0 => Err(PACEStatus::TriesLeft((v as u8) & 0xF)),
|
||||||
0x63C1 => PACEStatus::PasswordSuspended,
|
0x63C1 => Err(PACEStatus::PasswordSuspended),
|
||||||
0x63C0 => PACEStatus::PasswordBlocked,
|
0x63C0 => Err(PACEStatus::PasswordBlocked),
|
||||||
|
|
||||||
v => PACEStatus::Error(v),
|
v => Err(PACEStatus::Error(v)),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn step_general_authenticate(
|
pub async fn step_general_authenticate(
|
||||||
card: &mut impl Card,
|
card: &mut impl Card,
|
||||||
chained: bool,
|
chained: bool,
|
||||||
make_data: impl FnOnce(&mut Vec<u8>),
|
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();
|
let mut buf = Vec::new();
|
||||||
make_data(&mut buf);
|
make_data(&mut buf);
|
||||||
prepend_do(&mut buf, 0x7c);
|
prepend_do(&mut buf, 0x7c);
|
||||||
|
|
@ -398,7 +404,16 @@ pub async fn step_general_authenticate(
|
||||||
})
|
})
|
||||||
.await?;
|
.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());
|
return Ok(HashMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,7 +440,7 @@ pub async fn authenticate_pin(
|
||||||
card: &mut impl Card,
|
card: &mut impl Card,
|
||||||
pin: &[u8],
|
pin: &[u8],
|
||||||
cryptographic_mechanism: ObjectIdentifier,
|
cryptographic_mechanism: ObjectIdentifier,
|
||||||
) -> std::io::Result<PACECredentials> {
|
) -> Result<PACECredentials, PACEStatus> {
|
||||||
// Step one: Get the encrypted nonce
|
// Step one: Get the encrypted nonce
|
||||||
let mut data = step_general_authenticate(card, true, |_| {}).await?;
|
let mut data = step_general_authenticate(card, true, |_| {}).await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,7 @@ impl Card for PCSCCard {
|
||||||
apdu_buf.push(apdu.expected_length.unwrap_or_default() as u8);
|
apdu_buf.push(apdu.expected_length.unwrap_or_default() as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret_len = self
|
self.transmit_raw(&apdu_buf).await
|
||||||
.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 })
|
|
||||||
}
|
}
|
||||||
async fn transmit_raw(&mut self, apdu_buf: &[u8]) -> std::io::Result<crate::ResultAPDU> {
|
async fn transmit_raw(&mut self, apdu_buf: &[u8]) -> std::io::Result<crate::ResultAPDU> {
|
||||||
let ret_len = self
|
let ret_len = self
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue