Skip to content

Commit 7dc05c5

Browse files
SaBie73tobias-schwerdtfegerSaGematik
authored
OPEN-99: read cvc (#39)
read cv certificate --------- Co-authored-by: Tobias Schwerdtfeger <this@tobias-schwerdtfeger.dev> Co-authored-by: sandra.bieseke <sandra.bieseke@gematik.de>
1 parent 8cd2031 commit 7dc05c5

File tree

6 files changed

+132
-21
lines changed

6 files changed

+132
-21
lines changed

core-modules/healthcard/Cargo.toml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ once_cell = "1.21.3"
4545
num-bigint = "0.4"
4646
zeroize = { version = "1.8.1", features = ["zeroize_derive"] }
4747
uniffi = { version = "0.30.0", optional = true }
48-
clap = { version = "4.5.52", features = ["derive"], optional = true }
49-
pcsc = { version = "2.9.0", optional = true }
50-
serde = { version = "1.0.228", features = ["derive"], optional = true }
51-
serde_json = { version = "1.0.145", optional = true }
52-
53-
[dev-dependencies]
54-
serde = { version = "1.0.228", features = ["derive"] }
55-
serde_json = "1.0.145"
48+
clap = { version = "4.5.40", optional = true, features = ["derive"] }
49+
serde = { version = "1.0.219", optional = true, features = ["derive"] }
50+
serde_json = { version = "1.0.117", optional = true }
51+
pcsc = { version = "2.8.2", optional = true }

core-modules/healthcard/src/bin/apdu_record.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn main() {
2929
use clap::Parser;
3030
use crypto::ec::ec_key::{EcCurve, EcKeyPairSpec};
3131
use healthcard::exchange::apdu_tools::{PcscChannel, RecordingChannel};
32+
use healthcard::exchange::certificate::{retrieve_certificate_from, CertificateFile};
3233
use healthcard::exchange::secure_channel::{establish_secure_channel_with, CardAccessNumber};
3334

3435
if let Err(err) = run() {
@@ -58,6 +59,9 @@ fn main() {
5859
/// List available PC/SC readers and exit
5960
#[arg(long)]
6061
list_readers: bool,
62+
/// Read certificates and print them as hex to stdout
63+
#[arg(long)]
64+
read_certificates: bool,
6165
}
6266

6367
fn run() -> Result<(), String> {
@@ -80,13 +84,25 @@ fn main() {
8084
recorder.set_can(can.clone());
8185

8286
let mut generated_keys = Vec::new();
83-
establish_secure_channel_with(&mut recorder, &card_access_number, |curve: EcCurve| {
87+
let mut secure_channel = establish_secure_channel_with(&mut recorder, &card_access_number, |curve: EcCurve| {
8488
let (public_key, private_key) = EcKeyPairSpec { curve: curve.clone() }.generate_keypair()?;
8589
generated_keys.push(hex::encode_upper(private_key.as_bytes()));
8690
Ok((public_key, private_key))
8791
})
8892
.map_err(|err| format!("PACE failed: {err}"))?;
8993

94+
if args.read_certificates {
95+
let cert = retrieve_certificate_from(&mut secure_channel, CertificateFile::ChAutE256)
96+
.map_err(|err| format!("read DF.ESIGN/EF.C.CH.AUT.E256 failed: {err}"))?;
97+
print_certificate("DF.ESIGN/EF.C.CH.AUT.E256", &cert);
98+
99+
let cert = retrieve_certificate_from(&mut secure_channel, CertificateFile::EgkAutCvcE256)
100+
.map_err(|err| format!("read MF/EF.C.eGK.AUT_CVC.E256 failed: {err}"))?;
101+
print_certificate("MF/EF.C.eGK.AUT_CVC.E256", &cert);
102+
}
103+
104+
drop(secure_channel);
105+
90106
if !generated_keys.is_empty() {
91107
recorder.set_keys(generated_keys);
92108
}
@@ -96,6 +112,13 @@ fn main() {
96112
Ok(())
97113
}
98114

115+
fn print_certificate(label: &str, data: &[u8]) {
116+
println!("{label} ({} bytes):", data.len());
117+
for chunk in data.chunks(32) {
118+
println!(" {}", hex::encode_upper(chunk));
119+
}
120+
}
121+
99122
fn list_pcsc_readers() -> Result<(), String> {
100123
let ctx = pcsc::Context::establish(pcsc::Scope::User)
101124
.map_err(|err| format!("pcsc context establish failed: {err}"))?;

core-modules/healthcard/src/exchange/apdu_tools.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ impl Transcript {
124124
pub fn fixed_key_generator(&self) -> Result<Option<EcKeyPairGenerator>, TranscriptError> {
125125
match &self.header.keys {
126126
Some(keys) => {
127-
let decoded = keys.iter().map(hex::decode).collect::<Result<Vec<_>, _>>()?;
128-
Ok(Some(FixedKeyGenerator::new(decoded).generator()))
127+
let decoded = keys.iter().map(hex::decode).collect::<Result<Vec<_>, hex::FromHexError>>()?;
128+
Ok(Some(Box::new(FixedKeyGenerator::new(decoded).generator())))
129129
}
130130
None => Ok(None),
131131
}
@@ -371,8 +371,8 @@ impl FixedKeyGenerator {
371371
Self { keys }
372372
}
373373

374-
pub fn generator(mut self) -> EcKeyPairGenerator {
375-
Box::new(move |curve| {
374+
pub fn generator(mut self) -> impl FnMut(EcCurve) -> Result<(EcPublicKey, EcPrivateKey), CryptoError> {
375+
move |curve| {
376376
if self.keys.is_empty() {
377377
return Err(CryptoError::InvalidKeyMaterial { context: "fixed key generator ran out of keys" });
378378
}
@@ -381,7 +381,7 @@ impl FixedKeyGenerator {
381381
let (public_key, private_key) = derive_keypair_from_scalar(curve.clone(), bytes)?;
382382
eprintln!("FixedKeyGenerator used key for {curve:?}: {key_hex}");
383383
Ok((public_key, private_key))
384-
})
384+
}
385385
}
386386
}
387387

core-modules/healthcard/src/exchange/certificate.rs

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,43 @@ use super::channel::CardChannelExt;
2929
use super::error::ExchangeError;
3030
use super::ids;
3131

32+
/// Defines which certificate file to read from the card.
33+
#[derive(Clone, Copy, Debug)]
34+
pub enum CertificateFile {
35+
/// X.509 certificate stored in `DF.ESIGN/EF.C.CH.AUT.E256`.
36+
ChAutE256,
37+
/// CV certificate stored in `MF/EF.C.eGK.AUT_CVC.E256`.
38+
EgkAutCvcE256,
39+
}
40+
41+
fn select_certificate_file<S>(session: &mut S, certificate: CertificateFile) -> Result<(), ExchangeError>
42+
where
43+
S: CardChannelExt,
44+
{
45+
match certificate {
46+
CertificateFile::ChAutE256 => {
47+
session.execute_command_success(&HealthCardCommand::select_aid(&ids::df_esign_aid()))?;
48+
session.execute_command_success(&HealthCardCommand::select_fid_with_options(
49+
&ids::ef_cch_aut_e256_fid(),
50+
false,
51+
true,
52+
EXPECTED_LENGTH_WILDCARD_EXTENDED as i32,
53+
))?;
54+
}
55+
CertificateFile::EgkAutCvcE256 => {
56+
session.execute_command_success(&HealthCardCommand::select(false, false))?;
57+
session.execute_command_success(&HealthCardCommand::select_fid_with_options(
58+
&ids::ef_c_egk_aut_cvc_e256_fid(),
59+
false,
60+
true,
61+
EXPECTED_LENGTH_WILDCARD_EXTENDED as i32,
62+
))?;
63+
}
64+
}
65+
66+
Ok(())
67+
}
68+
3269
/// Retrieve the X.509 certificate stored in `DF.ESIGN/EF.C.CH.AUT.E256`.
3370
///
3471
/// The certificate is read in chunks using the READ BINARY command until the
@@ -37,13 +74,18 @@ pub fn retrieve_certificate<S>(session: &mut S) -> Result<Vec<u8>, ExchangeError
3774
where
3875
S: CardChannelExt,
3976
{
40-
session.execute_command_success(&HealthCardCommand::select_aid(&ids::df_esign_aid()))?;
41-
session.execute_command_success(&HealthCardCommand::select_fid_with_options(
42-
&ids::ef_cch_aut_e256_fid(),
43-
false,
44-
true,
45-
EXPECTED_LENGTH_WILDCARD_EXTENDED as i32,
46-
))?;
77+
retrieve_certificate_from(session, CertificateFile::ChAutE256)
78+
}
79+
80+
/// Retrieve a certificate file from the card.
81+
///
82+
/// The certificate is read in chunks using the READ BINARY command until the
83+
/// card indicates the end of the file.
84+
pub fn retrieve_certificate_from<S>(session: &mut S, certificate: CertificateFile) -> Result<Vec<u8>, ExchangeError>
85+
where
86+
S: CardChannelExt,
87+
{
88+
select_certificate_file(session, certificate)?;
4789

4890
let mut certificate = Vec::new();
4991
let mut offset: i32 = 0;
@@ -71,6 +113,7 @@ where
71113
mod tests {
72114
use super::*;
73115
use crate::command::health_card_status::HealthCardResponseStatus;
116+
use crate::command::select_command::SelectCommand;
74117
use crate::exchange::test_utils::MockSession;
75118

76119
#[test]
@@ -95,4 +138,31 @@ mod tests {
95138
other => panic!("unexpected error {other:?}"),
96139
}
97140
}
141+
142+
#[test]
143+
fn cv_certificate_selects_master_file() {
144+
let mut session = MockSession::with_extended_support(
145+
vec![vec![0x90, 0x00], vec![0x90, 0x00], vec![0xDE, 0xAD, 0x90, 0x00], vec![0xBE, 0xEF, 0x62, 0x82]],
146+
true,
147+
);
148+
149+
let cert = retrieve_certificate_from(&mut session, CertificateFile::EgkAutCvcE256).unwrap();
150+
assert_eq!(cert, vec![0xDE, 0xAD, 0xBE, 0xEF]);
151+
assert_eq!(
152+
session.recorded[0],
153+
HealthCardCommand::select(false, false).command_apdu(false).unwrap().to_bytes()
154+
);
155+
assert_eq!(
156+
session.recorded[1],
157+
HealthCardCommand::select_fid_with_options(
158+
&ids::ef_c_egk_aut_cvc_e256_fid(),
159+
false,
160+
true,
161+
EXPECTED_LENGTH_WILDCARD_EXTENDED as i32,
162+
)
163+
.command_apdu(false)
164+
.unwrap()
165+
.to_bytes()
166+
);
167+
}
98168
}

core-modules/healthcard/src/exchange/ids.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ pub fn ef_cch_aut_e256_sfid() -> ShortFileIdentifier {
102102
short_file_identifier(0x04)
103103
}
104104

105+
/// File identifier for `MF/EF.C.eGK.AUT_CVC.E256` (gemSpec_ObjSys Section 5.3.4).
106+
pub fn ef_c_egk_aut_cvc_e256_fid() -> FileIdentifier {
107+
file_identifier(0x2F06)
108+
}
109+
110+
/// Short file identifier for `MF/EF.C.eGK.AUT_CVC.E256` (gemSpec_ObjSys Section 5.3.4).
111+
pub fn ef_c_egk_aut_cvc_e256_sfid() -> ShortFileIdentifier {
112+
short_file_identifier(0x06)
113+
}
114+
105115
/// Key identifier for the `PrK.CH.AUT.E256` private key in `DF.ESIGN`.
106116
pub fn prk_ch_aut_e256() -> CardKey {
107117
CardKey::new(0x04).expect("constant key id must be valid")
@@ -135,6 +145,8 @@ mod tests {
135145
assert_eq!(ef_status_vd_sfid().value(), 0x0C);
136146
assert_eq!(ef_cch_aut_e256_fid().to_bytes(), [0xC5, 0x04]);
137147
assert_eq!(ef_cch_aut_e256_sfid().value(), 0x04);
148+
assert_eq!(ef_c_egk_aut_cvc_e256_fid().to_bytes(), [0x2F, 0x06]);
149+
assert_eq!(ef_c_egk_aut_cvc_e256_sfid().value(), 0x06);
138150

139151
assert_eq!(prk_ch_aut_e256().key_id(), 0x04);
140152
assert_eq!(mr_pin_home_reference().pwd_id(), 0x02);

docs/tooling/apdu-tools.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ cargo run -p healthcard --bin apdu_record --features apdu-tools,pcsc -- \
6969
--out ./transcript.jsonl
7070
```
7171

72+
To additionally read the certificates and print them to the console:
73+
74+
```sh
75+
cargo run -p healthcard --bin apdu_record --features apdu-tools,pcsc -- \
76+
--reader "<PCSC reader name>" \
77+
--can 123123 \
78+
--out ./transcript.jsonl \
79+
--read-certificates
80+
```
81+
7282
APDU length options:
7383

7484
- Default: uses extended-length APDUs when needed.

0 commit comments

Comments
 (0)