diff --git a/core-modules/healthcard/Cargo.toml b/core-modules/healthcard/Cargo.toml index d59f01d..961815b 100644 --- a/core-modules/healthcard/Cargo.toml +++ b/core-modules/healthcard/Cargo.toml @@ -45,11 +45,7 @@ once_cell = "1.21.3" num-bigint = "0.4" zeroize = { version = "1.8.1", features = ["zeroize_derive"] } uniffi = { version = "0.30.0", optional = true } -clap = { version = "4.5.52", features = ["derive"], optional = true } -pcsc = { version = "2.9.0", optional = true } -serde = { version = "1.0.228", features = ["derive"], optional = true } -serde_json = { version = "1.0.145", optional = true } - -[dev-dependencies] -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" +clap = { version = "4.5.40", optional = true, features = ["derive"] } +serde = { version = "1.0.219", optional = true, features = ["derive"] } +serde_json = { version = "1.0.117", optional = true } +pcsc = { version = "2.8.2", optional = true } diff --git a/core-modules/healthcard/src/bin/apdu_record.rs b/core-modules/healthcard/src/bin/apdu_record.rs index f3bb3ab..6a86cb2 100644 --- a/core-modules/healthcard/src/bin/apdu_record.rs +++ b/core-modules/healthcard/src/bin/apdu_record.rs @@ -29,6 +29,7 @@ fn main() { use clap::Parser; use crypto::ec::ec_key::{EcCurve, EcKeyPairSpec}; use healthcard::exchange::apdu_tools::{PcscChannel, RecordingChannel}; + use healthcard::exchange::certificate::{retrieve_certificate_from, CertificateFile}; use healthcard::exchange::secure_channel::{establish_secure_channel_with, CardAccessNumber}; if let Err(err) = run() { @@ -58,6 +59,9 @@ fn main() { /// List available PC/SC readers and exit #[arg(long)] list_readers: bool, + /// Read certificates and print them as hex to stdout + #[arg(long)] + read_certificates: bool, } fn run() -> Result<(), String> { @@ -80,13 +84,25 @@ fn main() { recorder.set_can(can.clone()); let mut generated_keys = Vec::new(); - establish_secure_channel_with(&mut recorder, &card_access_number, |curve: EcCurve| { + let mut secure_channel = establish_secure_channel_with(&mut recorder, &card_access_number, |curve: EcCurve| { let (public_key, private_key) = EcKeyPairSpec { curve: curve.clone() }.generate_keypair()?; generated_keys.push(hex::encode_upper(private_key.as_bytes())); Ok((public_key, private_key)) }) .map_err(|err| format!("PACE failed: {err}"))?; + if args.read_certificates { + let cert = retrieve_certificate_from(&mut secure_channel, CertificateFile::ChAutE256) + .map_err(|err| format!("read DF.ESIGN/EF.C.CH.AUT.E256 failed: {err}"))?; + print_certificate("DF.ESIGN/EF.C.CH.AUT.E256", &cert); + + let cert = retrieve_certificate_from(&mut secure_channel, CertificateFile::EgkAutCvcE256) + .map_err(|err| format!("read MF/EF.C.eGK.AUT_CVC.E256 failed: {err}"))?; + print_certificate("MF/EF.C.eGK.AUT_CVC.E256", &cert); + } + + drop(secure_channel); + if !generated_keys.is_empty() { recorder.set_keys(generated_keys); } @@ -96,6 +112,13 @@ fn main() { Ok(()) } + fn print_certificate(label: &str, data: &[u8]) { + println!("{label} ({} bytes):", data.len()); + for chunk in data.chunks(32) { + println!(" {}", hex::encode_upper(chunk)); + } + } + fn list_pcsc_readers() -> Result<(), String> { let ctx = pcsc::Context::establish(pcsc::Scope::User) .map_err(|err| format!("pcsc context establish failed: {err}"))?; diff --git a/core-modules/healthcard/src/exchange/apdu_tools.rs b/core-modules/healthcard/src/exchange/apdu_tools.rs index c0eda6d..95c9fe4 100644 --- a/core-modules/healthcard/src/exchange/apdu_tools.rs +++ b/core-modules/healthcard/src/exchange/apdu_tools.rs @@ -124,8 +124,8 @@ impl Transcript { pub fn fixed_key_generator(&self) -> Result, TranscriptError> { match &self.header.keys { Some(keys) => { - let decoded = keys.iter().map(hex::decode).collect::, _>>()?; - Ok(Some(FixedKeyGenerator::new(decoded).generator())) + let decoded = keys.iter().map(hex::decode).collect::, hex::FromHexError>>()?; + Ok(Some(Box::new(FixedKeyGenerator::new(decoded).generator()))) } None => Ok(None), } @@ -371,8 +371,8 @@ impl FixedKeyGenerator { Self { keys } } - pub fn generator(mut self) -> EcKeyPairGenerator { - Box::new(move |curve| { + pub fn generator(mut self) -> impl FnMut(EcCurve) -> Result<(EcPublicKey, EcPrivateKey), CryptoError> { + move |curve| { if self.keys.is_empty() { return Err(CryptoError::InvalidKeyMaterial { context: "fixed key generator ran out of keys" }); } @@ -381,7 +381,7 @@ impl FixedKeyGenerator { let (public_key, private_key) = derive_keypair_from_scalar(curve.clone(), bytes)?; eprintln!("FixedKeyGenerator used key for {curve:?}: {key_hex}"); Ok((public_key, private_key)) - }) + } } } diff --git a/core-modules/healthcard/src/exchange/certificate.rs b/core-modules/healthcard/src/exchange/certificate.rs index 8c54e64..0937a14 100644 --- a/core-modules/healthcard/src/exchange/certificate.rs +++ b/core-modules/healthcard/src/exchange/certificate.rs @@ -29,6 +29,43 @@ use super::channel::CardChannelExt; use super::error::ExchangeError; use super::ids; +/// Defines which certificate file to read from the card. +#[derive(Clone, Copy, Debug)] +pub enum CertificateFile { + /// X.509 certificate stored in `DF.ESIGN/EF.C.CH.AUT.E256`. + ChAutE256, + /// CV certificate stored in `MF/EF.C.eGK.AUT_CVC.E256`. + EgkAutCvcE256, +} + +fn select_certificate_file(session: &mut S, certificate: CertificateFile) -> Result<(), ExchangeError> +where + S: CardChannelExt, +{ + match certificate { + CertificateFile::ChAutE256 => { + session.execute_command_success(&HealthCardCommand::select_aid(&ids::df_esign_aid()))?; + session.execute_command_success(&HealthCardCommand::select_fid_with_options( + &ids::ef_cch_aut_e256_fid(), + false, + true, + EXPECTED_LENGTH_WILDCARD_EXTENDED as i32, + ))?; + } + CertificateFile::EgkAutCvcE256 => { + session.execute_command_success(&HealthCardCommand::select(false, false))?; + session.execute_command_success(&HealthCardCommand::select_fid_with_options( + &ids::ef_c_egk_aut_cvc_e256_fid(), + false, + true, + EXPECTED_LENGTH_WILDCARD_EXTENDED as i32, + ))?; + } + } + + Ok(()) +} + /// Retrieve the X.509 certificate stored in `DF.ESIGN/EF.C.CH.AUT.E256`. /// /// The certificate is read in chunks using the READ BINARY command until the @@ -37,13 +74,18 @@ pub fn retrieve_certificate(session: &mut S) -> Result, ExchangeError where S: CardChannelExt, { - session.execute_command_success(&HealthCardCommand::select_aid(&ids::df_esign_aid()))?; - session.execute_command_success(&HealthCardCommand::select_fid_with_options( - &ids::ef_cch_aut_e256_fid(), - false, - true, - EXPECTED_LENGTH_WILDCARD_EXTENDED as i32, - ))?; + retrieve_certificate_from(session, CertificateFile::ChAutE256) +} + +/// Retrieve a certificate file from the card. +/// +/// The certificate is read in chunks using the READ BINARY command until the +/// card indicates the end of the file. +pub fn retrieve_certificate_from(session: &mut S, certificate: CertificateFile) -> Result, ExchangeError> +where + S: CardChannelExt, +{ + select_certificate_file(session, certificate)?; let mut certificate = Vec::new(); let mut offset: i32 = 0; @@ -71,6 +113,7 @@ where mod tests { use super::*; use crate::command::health_card_status::HealthCardResponseStatus; + use crate::command::select_command::SelectCommand; use crate::exchange::test_utils::MockSession; #[test] @@ -95,4 +138,31 @@ mod tests { other => panic!("unexpected error {other:?}"), } } + + #[test] + fn cv_certificate_selects_master_file() { + let mut session = MockSession::with_extended_support( + vec![vec![0x90, 0x00], vec![0x90, 0x00], vec![0xDE, 0xAD, 0x90, 0x00], vec![0xBE, 0xEF, 0x62, 0x82]], + true, + ); + + let cert = retrieve_certificate_from(&mut session, CertificateFile::EgkAutCvcE256).unwrap(); + assert_eq!(cert, vec![0xDE, 0xAD, 0xBE, 0xEF]); + assert_eq!( + session.recorded[0], + HealthCardCommand::select(false, false).command_apdu(false).unwrap().to_bytes() + ); + assert_eq!( + session.recorded[1], + HealthCardCommand::select_fid_with_options( + &ids::ef_c_egk_aut_cvc_e256_fid(), + false, + true, + EXPECTED_LENGTH_WILDCARD_EXTENDED as i32, + ) + .command_apdu(false) + .unwrap() + .to_bytes() + ); + } } diff --git a/core-modules/healthcard/src/exchange/ids.rs b/core-modules/healthcard/src/exchange/ids.rs index 89e9bc0..61e4fe9 100644 --- a/core-modules/healthcard/src/exchange/ids.rs +++ b/core-modules/healthcard/src/exchange/ids.rs @@ -102,6 +102,16 @@ pub fn ef_cch_aut_e256_sfid() -> ShortFileIdentifier { short_file_identifier(0x04) } +/// File identifier for `MF/EF.C.eGK.AUT_CVC.E256` (gemSpec_ObjSys Section 5.3.4). +pub fn ef_c_egk_aut_cvc_e256_fid() -> FileIdentifier { + file_identifier(0x2F06) +} + +/// Short file identifier for `MF/EF.C.eGK.AUT_CVC.E256` (gemSpec_ObjSys Section 5.3.4). +pub fn ef_c_egk_aut_cvc_e256_sfid() -> ShortFileIdentifier { + short_file_identifier(0x06) +} + /// Key identifier for the `PrK.CH.AUT.E256` private key in `DF.ESIGN`. pub fn prk_ch_aut_e256() -> CardKey { CardKey::new(0x04).expect("constant key id must be valid") @@ -135,6 +145,8 @@ mod tests { assert_eq!(ef_status_vd_sfid().value(), 0x0C); assert_eq!(ef_cch_aut_e256_fid().to_bytes(), [0xC5, 0x04]); assert_eq!(ef_cch_aut_e256_sfid().value(), 0x04); + assert_eq!(ef_c_egk_aut_cvc_e256_fid().to_bytes(), [0x2F, 0x06]); + assert_eq!(ef_c_egk_aut_cvc_e256_sfid().value(), 0x06); assert_eq!(prk_ch_aut_e256().key_id(), 0x04); assert_eq!(mr_pin_home_reference().pwd_id(), 0x02); diff --git a/docs/tooling/apdu-tools.md b/docs/tooling/apdu-tools.md index 7969530..466423d 100644 --- a/docs/tooling/apdu-tools.md +++ b/docs/tooling/apdu-tools.md @@ -69,6 +69,16 @@ cargo run -p healthcard --bin apdu_record --features apdu-tools,pcsc -- \ --out ./transcript.jsonl ``` +To additionally read the certificates and print them to the console: + +```sh +cargo run -p healthcard --bin apdu_record --features apdu-tools,pcsc -- \ + --reader "" \ + --can 123123 \ + --out ./transcript.jsonl \ + --read-certificates +``` + APDU length options: - Default: uses extended-length APDUs when needed.