Skip to content

Commit dea60b5

Browse files
fix(ffi): implement YubiKey-related cache items for WinSCard (#531)
1 parent d6be374 commit dea60b5

6 files changed

Lines changed: 192 additions & 21 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/ffi-types/src/common.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub type Bool = i32;
2424
/// unsigned char Data4[8];
2525
/// } GUID;
2626
/// ```
27-
#[derive(Clone, Copy)]
27+
#[derive(Clone, Copy, Debug)]
2828
#[repr(C)]
2929
pub struct Guid {
3030
pub data1: u32,

‎ffi/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cfg-if.workspace = true
2525
num-traits = { workspace = true, default-features = true }
2626
whoami.workspace = true
2727
sha1.workspace = true
28+
sha2.workspace = true
2829
ffi-types = { workspace = true, features = ["winscard"], optional = true }
2930

3031
picky = { workspace = true, features = ["x509"], optional = true }

‎ffi/src/winscard/piv.rs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,53 @@ const CERTIFICATE_LABELS: &[(&[u8], [u8; 3])] = &[
3434
),
3535
];
3636

37+
/// PIV certificate slot ID.
38+
///
39+
/// Read more in the NIST SP 800-73pt1-5 specification: Table 5. PIV Card Application key references.
40+
#[derive(Debug, Clone, Copy)]
41+
#[repr(u8)]
42+
pub enum SlotId {
43+
/// PIV Authentication Key.
44+
PivAuthentication = 0x9a,
45+
/// Digital Signature Key.
46+
DigitalSignature = 0x9c,
47+
/// Key Management Key.
48+
KeyManagement = 0x9d,
49+
/// Card Authentication Key.
50+
CardAuthentication = 0x9e,
51+
}
52+
53+
impl SlotId {
54+
/// Returns [u8] representation of the slot.
55+
pub fn as_u8(&self) -> u8 {
56+
*self as u8
57+
}
58+
59+
/// Determines the slot id based on the slot certificate label.
60+
pub fn from_certificate_label(certificate_label: &[u8]) -> Result<Self> {
61+
for (label, slot_id) in CERTIFICATE_SLOT_IDS {
62+
if *label == certificate_label {
63+
return Ok(*slot_id);
64+
}
65+
}
66+
67+
Err(Error::new(ErrorKind::NoCredentials, "certificate label not recognized"))
68+
}
69+
}
70+
71+
/// Possible PIV slot IDs.
72+
///
73+
/// The PIV (Personal Identity Verification) standard specifies 25 slots. Each slot has a name and number.
74+
/// Each slot number is given as a hex value, and all slot numbers can be represented as a single byte.
75+
///
76+
/// More info: https://docs.yubico.com/yesdk/users-manual/application-piv/slots.html.
77+
const CERTIFICATE_SLOT_IDS: &[(&[u8], SlotId)] = &[
78+
(b"X.509 Certificate for PIV Authentication", SlotId::PivAuthentication),
79+
(b"X.509 Certificate for Digital Signature", SlotId::DigitalSignature),
80+
(b"X.509 Certificate for Key Management", SlotId::KeyManagement),
81+
(b"X.509 Certificate for Card Authentication", SlotId::CardAuthentication),
82+
];
83+
3784
/// SELECT APDU command.
3885
///
3986
/// The SELECT card command sets the currently selected application. More info:

‎ffi/src/winscard/scard_context.rs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,6 @@ unsafe fn write_cache(
10071007
) -> WinScardResult<()> {
10081008
check_handle!(context, "scard context handle");
10091009
check_null!(card_identifier, "card identified");
1010-
check_null!(data, "cache data buffer");
10111010

10121011
// SAFETY: The `card_identifier` parameter is not null (checked above).
10131012
let card_id = unsafe {
@@ -1021,8 +1020,14 @@ unsafe fn write_cache(
10211020

10221021
// SAFETY: The `context` value is not zero (checked above).
10231022
let context = unsafe { scard_context_to_winscard_context(context) }?;
1024-
// SAFETY: The `data` parameter is not null (checked above).
1025-
let data = unsafe { from_raw_parts(data, data_len.try_into()?) }.to_vec();
1023+
// The YubiKey Smart Card Minidriver can call `SCardWriteCacheW` with the `Data` pointer set to NULL and
1024+
// `DataLen` equal to 0.
1025+
let data = if data.is_null() || data_len == 0 {
1026+
Vec::new()
1027+
} else {
1028+
// SAFETY: The `data` parameter is not null (checked above).
1029+
unsafe { from_raw_parts(data, data_len.try_into()?) }.to_vec()
1030+
};
10261031

10271032
context.write_cache(card_id, freshness_counter, lookup_name.to_owned(), data)
10281033
}

‎ffi/src/winscard/system_scard/context.rs‎

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,19 @@ impl SystemScardContext {
8585

8686
#[cfg(not(target_os = "windows"))]
8787
if _with_cache {
88-
let (container_name, auth_cert, auth_cert_der) = if let (Ok(auth_cert), Ok(container_name)) =
88+
let scard_logon_params = if let (Ok(certificate), Ok(container_name)) =
8989
(winscard::env::auth_cert_from_env(), winscard::env::container_name())
9090
{
91-
let auth_cert_der = auth_cert.to_der()?;
91+
use crate::winscard::piv::SlotId;
9292

93-
(container_name, auth_cert, auth_cert_der)
93+
let certificate_der = certificate.to_der()?;
94+
95+
ScardLogonParams {
96+
container_name,
97+
certificate,
98+
certificate_der,
99+
slot: SlotId::PivAuthentication,
100+
}
94101
} else {
95102
use std::path::Path;
96103

@@ -102,23 +109,29 @@ impl SystemScardContext {
102109
try_extract_container_name_and_certificate(Path::new(&pkcs11_module_path))?
103110
};
104111

105-
scard_context.cache = init_scard_cache(&container_name, auth_cert, &auth_cert_der)?;
112+
scard_context.cache = init_scard_cache(&scard_logon_params)?;
106113
}
107114

108115
Ok(scard_context)
109116
}
110117
}
111118

119+
#[cfg(not(target_os = "windows"))]
120+
struct ScardLogonParams {
121+
container_name: String,
122+
certificate: picky::x509::Cert,
123+
certificate_der: Vec<u8>,
124+
slot: crate::winscard::piv::SlotId,
125+
}
126+
112127
#[cfg(not(target_os = "windows"))]
113128
#[instrument(err)]
114-
pub fn try_extract_container_name_and_certificate(
115-
pkcs11_module: &std::path::Path,
116-
) -> Result<(String, picky::x509::Cert, Vec<u8>), Error> {
129+
fn try_extract_container_name_and_certificate(pkcs11_module: &std::path::Path) -> Result<ScardLogonParams, Error> {
117130
use cryptoki::context::{CInitializeArgs, Pkcs11};
118131
use cryptoki::object::{Attribute, AttributeType, CertificateType, ObjectClass};
119132
use picky::x509::Cert;
120133

121-
use crate::winscard::piv::try_get_piv_container_name;
134+
use crate::winscard::piv::{try_get_piv_container_name, SlotId};
122135

123136
macro_rules! try_execute {
124137
($x:expr, $msg:literal) => {{
@@ -177,6 +190,7 @@ pub fn try_extract_container_name_and_certificate(
177190
};
178191

179192
let mut container_name = None;
193+
let mut slot = None;
180194
// We found a suitable certificate for smart card logon on the device.
181195
// Next, we check if the inserted device is a PIV smart card. If so, we will attempt
182196
// to extract the container name using raw APDU commands.
@@ -192,6 +206,10 @@ pub fn try_extract_container_name_and_certificate(
192206
try_get_piv_container_name(reader_name, &label),
193207
"failed to construct a container name"
194208
));
209+
slot = Some(try_execute!(
210+
SlotId::from_certificate_label(&label),
211+
"failed to determine a slot id"
212+
));
195213

196214
if container_name.is_some() {
197215
break;
@@ -202,12 +220,21 @@ pub fn try_extract_container_name_and_certificate(
202220
continue;
203221
};
204222

223+
let Some(slot) = slot else {
224+
continue;
225+
};
226+
205227
let certificate = try_execute!(
206228
Cert::from_der(&certificate_der),
207229
"failed to parse smart card certificate"
208230
);
209231

210-
return Ok((container_name, certificate, certificate_der));
232+
return Ok(ScardLogonParams {
233+
container_name,
234+
certificate,
235+
certificate_der,
236+
slot,
237+
});
211238
}
212239

213240
Err(Error::new(
@@ -253,11 +280,14 @@ fn validate_certificate(certificate: &[u8]) -> Result<(), Error> {
253280
}
254281

255282
#[cfg(not(target_os = "windows"))]
256-
fn init_scard_cache(
257-
container_name: &str,
258-
auth_cert: picky::x509::Cert,
259-
auth_cert_der: &[u8],
260-
) -> WinScardResult<BTreeMap<String, Vec<u8>>> {
283+
fn init_scard_cache(scard_logon_params: &ScardLogonParams) -> WinScardResult<BTreeMap<String, Vec<u8>>> {
284+
let ScardLogonParams {
285+
container_name,
286+
certificate,
287+
certificate_der,
288+
slot,
289+
} = scard_logon_params;
290+
261291
use picky_asn1_x509::{PublicKey, SubjectPublicKeyInfo};
262292

263293
// https://github.com/selfrender/Windows-Server-2003/blob/5c6fe3db626b63a384230a1aa6b92ac416b0765f/ds/security/csps/wfsccsp/inc/basecsp.h#L86-L93
@@ -388,7 +418,7 @@ fn init_scard_cache(
388418
value.extend_from_slice(b"RSA1"); // magic = RSA1
389419
value.extend_from_slice(&2048_u32.to_le_bytes()); // bitlen = 2048
390420

391-
let public_key = auth_cert.public_key();
421+
let public_key = certificate.public_key();
392422
let public_key: &SubjectPublicKeyInfo = public_key.as_ref();
393423
let (modulus, public_exponent) = match &public_key.subject_public_key {
394424
PublicKey::Rsa(rsa) => (
@@ -423,9 +453,9 @@ fn init_scard_cache(
423453
// unkown flags
424454
value.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
425455

426-
value.extend_from_slice(&(u16::try_from(auth_cert_der.len())?.to_le_bytes())); // uncompressed certificate data len
456+
value.extend_from_slice(&(u16::try_from(certificate_der.len())?.to_le_bytes())); // uncompressed certificate data len
427457
value.extend_from_slice(&[0x00, 0x00]); // flags that specify that the certificate is not compressed
428-
value.extend_from_slice(auth_cert_der);
458+
value.extend_from_slice(certificate_der);
429459

430460
value
431461
});
@@ -443,6 +473,93 @@ fn init_scard_cache(
443473
value
444474
});
445475

476+
//= YubiKey-related cache items =//
477+
478+
/// The KeySpec property identifies how a key that is generated or retrieved using the Microsoft CryptoAPI (CAPI)
479+
/// from a Microsoft legacy Cryptographic Storage Provider (CSP) can be used.
480+
///
481+
/// We need the [KeySpec] value to build scard cache items correctly.
482+
#[repr(u8)]
483+
enum KeySpec {
484+
/// RSA key that can be used for signing and decryption.
485+
AtKeyExchange = 1,
486+
/// RSA signature only key.
487+
AtSignature = 2,
488+
}
489+
490+
cache.insert("ykmd_cardcf".to_owned(), CACHE_ITEM_HEADER.to_vec());
491+
492+
cache.insert("ykmd_lastupdate".to_owned(), {
493+
use std::time::{SystemTime, UNIX_EPOCH};
494+
495+
let now = SystemTime::now();
496+
let epoch_seconds = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
497+
498+
epoch_seconds.to_le_bytes().to_vec()
499+
});
500+
501+
cache.insert("ykmd_cmap".to_owned(), {
502+
use ffi_types::WChar;
503+
use sha2::Digest;
504+
505+
use crate::winscard::piv::SlotId;
506+
507+
// This cache item represents the `ykpiv_container` object:
508+
//
509+
// https://github.com/Yubico/yubico-piv-tool/blob/6444d40d73f4d4c029c9739938edf57ede23e6f1/lib/ykpiv.h#L268-L277
510+
// ```C
511+
// typedef struct _ykpiv_container {
512+
// wchar_t name[40];
513+
// uint8_t slot;
514+
// uint8_t key_spec;
515+
// uint16_t key_size_bits;
516+
// uint8_t flags;
517+
// uint8_t pin_id;
518+
// uint8_t associated_echd_container;
519+
// uint8_t cert_fingerprint[20];
520+
// } ykpiv_container;
521+
// ```
522+
523+
let mut container_name_encoded = container_name
524+
.encode_utf16()
525+
.chain(core::iter::once(0))
526+
.flat_map(|v| v.to_le_bytes())
527+
.collect::<Vec<_>>();
528+
let container_name_bytes_len = size_of::<WChar>() * MAX_CONTAINER_NAME_LEN;
529+
debug_assert_eq!(container_name_bytes_len, 80);
530+
531+
container_name_encoded.resize(container_name_bytes_len, 0);
532+
debug!(?container_name_encoded);
533+
534+
let mut value = container_name_encoded;
535+
value.push(slot.as_u8()); // Slot.
536+
537+
// The KeySpec value is not encoded inside certificate. We determine it by looking at the certificate slot id.
538+
let key_spec = match slot {
539+
SlotId::PivAuthentication => KeySpec::AtKeyExchange,
540+
SlotId::DigitalSignature => KeySpec::AtSignature,
541+
SlotId::KeyManagement => KeySpec::AtKeyExchange,
542+
SlotId::CardAuthentication => KeySpec::AtKeyExchange,
543+
};
544+
value.push(key_spec as u8);
545+
546+
value.extend_from_slice(&[0x00, 0x08]); // KeySize.
547+
value.push(0x03); // Flags.
548+
value.push(0x01); // PIN id.
549+
value.push(0xff); // Associated ECHD container (none).
550+
551+
// Discovered experimentally:
552+
// The `cert_fingerprint` field contains the first 20 bytes of SHA256 cert hash (instead of SHA1 cert hash).
553+
let cert_fingerprint = sha2::Sha256::digest(certificate_der);
554+
value.extend_from_slice(&cert_fingerprint.as_slice()[0..20]);
555+
556+
// The value length must be the correct size. The YubiKey Smart Card Minidriver checks it.
557+
// The `0x6b` was extracted from the `ykmd.dll` and is also equal to the `ykpiv_container` size.
558+
debug_assert_eq!(value.len(), 0x6b);
559+
560+
value
561+
});
562+
446563
Ok(cache)
447564
}
448565

0 commit comments

Comments
 (0)