Skip to content

Commit

Permalink
feat(scard): credentials handling (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheBestTvarynka authored and CBenoit committed Feb 15, 2024
1 parent cfc86d6 commit 39f21f6
Show file tree
Hide file tree
Showing 25 changed files with 224 additions and 370 deletions.
17 changes: 8 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dns_resolver = ["dep:trust-dns-resolver", "dep:tokio", "tokio/rt-multi-thread"]
# TSSSP should be used only on Windows as a native CREDSSP replacement
tsssp = ["dep:rustls"]
# Turns on Kerberos smart card login (available only on Windows and users WinSCard API)
scard = ["dep:pcsc", "dep:winscard", "dep:iso7816-tlv"]
scard = ["dep:pcsc", "dep:winscard"]

[dependencies]
byteorder = "1.4"
Expand Down Expand Up @@ -70,7 +70,6 @@ tokio = { version = "1.32", features = ["time", "rt"], optional = true }
pcsc = { version = "2.8", optional = true }
async-recursion = "1.0.5"
winscard = { path = "./crates/winscard", optional = true }
iso7816-tlv = { version = "0.4.3", optional = true }

[target.'cfg(windows)'.dependencies]
winreg = "0.51"
Expand Down
2 changes: 1 addition & 1 deletion crates/winscard/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ name = "winscard"
bitflags = "2.4.0"
iso7816 = "0.1.1"
iso7816-tlv = "0.4.3"
picky = { version = "7.0.0-rc.8", default-features = false }
picky = { version = "7.0.0-rc.8", default-features = false, features = ["x509"] }
picky-asn1-x509 = { version = "0.12.0" }
tracing = { version = "0.1.37", features = ["attributes"], default-features = false }
time = { version = "0.3.28", default-features = false, features = [
Expand Down
7 changes: 7 additions & 0 deletions crates/winscard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use core::{fmt, result};
pub use ber_tlv::ber_tlv_length_encoding;
use iso7816_tlv::TlvError;
use picky::key::KeyError;
use picky::x509::certificate::CertError;
pub use scard::{SmartCard, ATR, PIV_AID};
pub use scard_context::{Reader, ScardContext, SmartCardInfo};

Expand Down Expand Up @@ -141,6 +142,12 @@ impl From<TryFromIntError> for Error {
}
}

impl From<CertError> for Error {
fn from(value: CertError) -> Self {
Error::new(ErrorKind::InsufficientBuffer, format!("certificate error: {}", value))
}
}

/// [Smart Card Return Values](https://learn.microsoft.com/en-us/windows/win32/secauthn/authentication-return-values).
#[derive(Debug, PartialEq)]
#[repr(u32)]
Expand Down
1 change: 1 addition & 0 deletions crates/winscard/src/macros.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(feature = "std")]
macro_rules! env {
($name:expr) => {{
std::env::var($name).map_err(|_| {
Expand Down
29 changes: 24 additions & 5 deletions crates/winscard/src/scard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub const ATR: [u8; 17] = [
/// Emulated smart card.
///
/// Currently, we support one key container per smart card.
#[derive(Debug, Clone)]
pub struct SmartCard<'a> {
reader_name: Cow<'a, str>,
chuid: [u8; CHUID_LENGTH],
Expand Down Expand Up @@ -101,7 +102,7 @@ impl SmartCard<'_> {
})
}

fn validate_and_pad_pin(mut pin: Vec<u8>) -> WinScardResult<Vec<u8>> {
fn validate_and_pad_pin(pin: Vec<u8>) -> WinScardResult<Vec<u8>> {
// All PIN requirements can be found here: NIST.SP.800-73-4 part 2, section 2.4.3
if !(PIN_LENGTH_RANGE_LOW_BOUND..=PIN_LENGTH_RANGE_HIGH_BOUND).contains(&pin.len()) {
return Err(Error::new(
Expand All @@ -116,13 +117,17 @@ impl SmartCard<'_> {
));
};

Ok(Self::pad_pin(pin))
}

fn pad_pin(mut pin: Vec<u8>) -> Vec<u8> {
if pin.len() < PIN_LENGTH_RANGE_HIGH_BOUND {
// NIST.SP.800-73-4 part 2, section 2.4.3
const PIN_PAD_VALUE: u8 = 0xFF;
pin.resize(PIN_LENGTH_RANGE_HIGH_BOUND, PIN_PAD_VALUE);
}

Ok(pin)
pin
}

/// This functions handles one APDU command.
Expand Down Expand Up @@ -342,9 +347,9 @@ impl SmartCard<'_> {
// NIST.SP.800-73-4, Part 1, Table 5
const RSA_ALGORITHM: u8 = 0x07;
// NIST.SP.800-73-4, Part 1, Table 4b
const PIV_AUTHENTICATION_KEY: u8 = 0x9A;
const PIV_DIGITAL_SIGNATURE_KEY: u8 = 0x9C;

if cmd.p1 != RSA_ALGORITHM || cmd.p2 != PIV_AUTHENTICATION_KEY {
if cmd.p1 != RSA_ALGORITHM || cmd.p2 != PIV_DIGITAL_SIGNATURE_KEY {
return Err(Error::new(
ErrorKind::UnsupportedFeature,
format!("Provided algorithm or key reference isn't supported: got algorithm {:x}, expected 0x07; got key reference {:x}, expected 0x9A", cmd.p1, cmd.p2)
Expand Down Expand Up @@ -428,6 +433,20 @@ impl SmartCard<'_> {
Ok(signature)
}

/// Verifies the PIN code. This method alters the scard state.
pub fn verify_pin(&mut self, pin: &[u8]) -> WinScardResult<()> {
if self.pin != Self::pad_pin(pin.into()) {
return Err(Error::new(
ErrorKind::InvalidValue,
"PIN verification error: Invalid PIN",
));
}

self.state = SCardState::PinVerified;

Ok(())
}

fn get_next_response_chunk(&mut self) -> Option<(Vec<u8>, usize)> {
let vec = self.pending_response.as_mut()?;
if vec.is_empty() {
Expand All @@ -439,7 +458,7 @@ impl SmartCard<'_> {
}
}

#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
enum SCardState {
Ready,
PivAppSelected,
Expand Down
13 changes: 11 additions & 2 deletions crates/winscard/src/scard_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use alloc::vec::Vec;
use alloc::{format, vec};

use picky::key::PrivateKey;
use picky::x509::Cert;
use picky_asn1_x509::{PublicKey, SubjectPublicKeyInfo};

use crate::scard::SmartCard;
Expand All @@ -24,13 +25,16 @@ pub struct Reader<'a> {
}

/// Describes smart card info used for the smart card creation.
#[derive(Debug, Clone)]
pub struct SmartCardInfo<'a> {
/// Container name which stores the certificate along with its private key.
pub container_name: Cow<'a, str>,
/// Smart card PIN code.
pub pin: Vec<u8>,
/// DER-encoded smart card certificate.
pub auth_cert_der: Vec<u8>,
/// Encoded private key (pem).
pub auth_pk_pem: Cow<'a, str>,
/// Private key.
pub auth_pk: PrivateKey,
/// Information about smart card reader.
Expand Down Expand Up @@ -58,12 +62,13 @@ impl<'a> SmartCardInfo<'a> {
let pin = env!(WINSCARD_PIN_ENV)?.into();

let cert_path = env!(WINSCARD_CERT_PATH_ENV)?;
let raw_certificate = fs::read(cert_path).map_err(|e| {
let raw_certificate = fs::read_to_string(cert_path).map_err(|e| {
Error::new(
ErrorKind::InvalidParameter,
format!("Unable to read certificate from the provided file: {}", e),
)
})?;
let auth_cert_der = Cert::from_pem_str(&raw_certificate)?.to_der()?;
let pk_path = env!(WINSCARD_PK_PATH_ENV)?;
let raw_private_key = fs::read_to_string(pk_path).map_err(|e| {
Error::new(
Expand Down Expand Up @@ -92,7 +97,8 @@ impl<'a> SmartCardInfo<'a> {
Ok(Self {
container_name,
pin,
auth_cert_der: raw_certificate,
auth_cert_der,
auth_pk_pem: raw_private_key.into(),
auth_pk: private_key,
reader,
})
Expand All @@ -104,6 +110,7 @@ impl<'a> SmartCardInfo<'a> {
reader_name: Cow<'a, str>,
pin: Vec<u8>,
auth_cert_der: Vec<u8>,
auth_pk_pem: Cow<'a, str>,
auth_pk: PrivateKey,
) -> Self {
// Standard Windows Reader Icon
Expand All @@ -117,6 +124,7 @@ impl<'a> SmartCardInfo<'a> {
container_name,
pin,
auth_cert_der,
auth_pk_pem,
auth_pk,
reader,
}
Expand All @@ -126,6 +134,7 @@ impl<'a> SmartCardInfo<'a> {
/// Represents the resource manager context (the scope).
///
/// Currently, we support only one smart card per smart card context.
#[derive(Debug, Clone)]
pub struct ScardContext<'a> {
smart_card_info: SmartCardInfo<'a>,
cache: BTreeMap<String, Vec<u8>>,
Expand Down
8 changes: 4 additions & 4 deletions examples/kerberos.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::error::Error;

use base64::Engine;
use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
Expand All @@ -7,11 +9,9 @@ use reqwest::StatusCode;
use sspi::builders::EmptyInitializeSecurityContext;
use sspi::{
AcquireCredentialsHandleResult, ClientRequestFlags, CredentialsBuffers, DataRepresentation,
InitializeSecurityContextResult, KerberosConfig, SecurityBuffer, SecurityBufferType, SecurityStatus, Sspi,
Username,
InitializeSecurityContextResult, Kerberos, KerberosConfig, SecurityBuffer, SecurityBufferType, SecurityStatus,
Sspi, SspiImpl, Username,
};
use sspi::{Kerberos, SspiImpl};
use std::error::Error;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};

Expand Down
18 changes: 17 additions & 1 deletion ffi/src/sspi/sec_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ pub(crate) unsafe fn p_ctxt_handle_to_sspi_context(
))?),
_ => {
return Err(Error::new(
ErrorKind::InvalidParameter,
ErrorKind::SecurityPackageNotFound,
format!("security package name `{}` is not supported", name),
));
}
Expand All @@ -188,6 +188,18 @@ pub(crate) unsafe fn p_ctxt_handle_to_sspi_context(
Ok((*(*context)).dw_lower as *mut SspiContext)
}

fn verify_security_package(package_name: &str) -> Result<()> {
match package_name {
negotiate::PKG_NAME | pku2u::PKG_NAME | kerberos::PKG_NAME | ntlm::PKG_NAME => Ok(()),
#[cfg(feature = "tsssp")]
sspi_cred_ssp::PKG_NAME => Ok(()),
_ => Err(Error::new(
ErrorKind::SecurityPackageNotFound,
format!("security package name `{}` is not supported", package_name),
)),
}
}

#[instrument(skip_all)]
#[cfg_attr(windows, rename_symbol(to = "Rust_AcquireCredentialsHandleA"))]
#[no_mangle]
Expand All @@ -209,6 +221,8 @@ pub unsafe extern "system" fn AcquireCredentialsHandleA(

let security_package_name =
try_execute!(CStr::from_ptr(psz_package).to_str(), ErrorKind::InvalidParameter).to_owned();
try_execute!(verify_security_package(&security_package_name));

debug!(?security_package_name);

let mut package_list: Option<String> = None;
Expand Down Expand Up @@ -257,6 +271,8 @@ pub unsafe extern "system" fn AcquireCredentialsHandleW(
check_null!(ph_credential);

let security_package_name = c_w_str_to_string(psz_package);
try_execute!(verify_security_package(&security_package_name));

debug!(?security_package_name);

let mut package_list: Option<String> = None;
Expand Down
8 changes: 4 additions & 4 deletions ffi/src/sspi/sec_pkg_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ pub type PSecPkgInfoA = *mut SecPkgInfoA;
impl From<PackageInfo> for &mut SecPkgInfoA {
fn from(pkg_info: PackageInfo) -> Self {
let mut pkg_name = pkg_info.name.to_string().as_bytes().to_vec();
// null-terminator
// We need to add the null-terminator during the conversion from Rust to C string.
pkg_name.push(0);
let name_bytes_len = pkg_name.len();

let mut pkg_comment = pkg_info.comment.as_bytes().to_vec();
// null-terminator
// We need to add the null-terminator during the conversion from Rust to C string.
pkg_comment.push(0);
let comment_bytes_len = pkg_comment.len();

Expand Down Expand Up @@ -177,14 +177,14 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesA(
pkg_info_a.cb_max_token = pkg_info.max_token_len;

let mut name = pkg_info.name.as_ref().as_bytes().to_vec();
// null-terminator
// We need to add the null-terminator during the conversion from Rust to C string.
name.push(0);
copy_nonoverlapping(name.as_ptr(), data_ptr as *mut _, name.len());
pkg_info_a.name = data_ptr as *mut _;
data_ptr = data_ptr.add(name.len());

let mut comment = pkg_info.comment.as_bytes().to_vec();
// null-terminator
// We need to add the null-terminator during the conversion from Rust to C string.
comment.push(0);
copy_nonoverlapping(comment.as_ptr(), data_ptr as *mut _, comment.len());
pkg_info_a.comment = data_ptr as *mut _;
Expand Down
Loading

0 comments on commit 39f21f6

Please sign in to comment.