Applets management (#568)
This commit is contained in:
Generated
+8
-1
@@ -93,6 +93,12 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.11.0-rc.3"
|
version = "0.11.0-rc.3"
|
||||||
@@ -549,7 +555,7 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4"
|
checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
"pcsc-sys",
|
"pcsc-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1094,6 +1100,7 @@ name = "yubikey"
|
|||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
"bitflags 2.5.0",
|
||||||
"der",
|
"der",
|
||||||
"des",
|
"des",
|
||||||
"ecdsa",
|
"ecdsa",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ sha2 = "=0.11.0-pre.4"
|
|||||||
x509-cert = { version = "=0.3.0-pre.0", features = [ "builder", "hazmat" ] }
|
x509-cert = { version = "=0.3.0-pre.0", features = [ "builder", "hazmat" ] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bitflags = "2.5.0"
|
||||||
der = "=0.8.0-rc.1"
|
der = "=0.8.0-rc.1"
|
||||||
des = "=0.9.0-pre.2"
|
des = "=0.9.0-pre.2"
|
||||||
elliptic-curve = "=0.14.0-rc.1"
|
elliptic-curve = "=0.14.0-rc.1"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#![cfg(feature = "untested")]
|
||||||
|
|
||||||
|
use yubikey::{mgm, YubiKey};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let yubikey = YubiKey::open().unwrap();
|
||||||
|
|
||||||
|
let mut mgmt = mgm::Manager::new(yubikey).unwrap();
|
||||||
|
mgmt.enable_yubihsm().unwrap();
|
||||||
|
}
|
||||||
+20
@@ -204,6 +204,15 @@ pub enum Ins {
|
|||||||
/// Get slot metadata
|
/// Get slot metadata
|
||||||
GetMetadata,
|
GetMetadata,
|
||||||
|
|
||||||
|
/// Management // Read Config
|
||||||
|
ReadConfig,
|
||||||
|
|
||||||
|
/// Management // Write Config
|
||||||
|
WriteConfig,
|
||||||
|
|
||||||
|
/// Management // DeviceReset
|
||||||
|
DeviceReset,
|
||||||
|
|
||||||
/// Other/unrecognized instruction codes
|
/// Other/unrecognized instruction codes
|
||||||
Other(u8),
|
Other(u8),
|
||||||
}
|
}
|
||||||
@@ -229,6 +238,12 @@ impl Ins {
|
|||||||
Ins::Attest => 0xf9,
|
Ins::Attest => 0xf9,
|
||||||
Ins::GetSerial => 0xf8,
|
Ins::GetSerial => 0xf8,
|
||||||
Ins::GetMetadata => 0xf7,
|
Ins::GetMetadata => 0xf7,
|
||||||
|
|
||||||
|
// Management
|
||||||
|
Ins::ReadConfig => 0x1d,
|
||||||
|
Ins::WriteConfig => 0x1c,
|
||||||
|
Ins::DeviceReset => 0x1f,
|
||||||
|
|
||||||
Ins::Other(code) => code,
|
Ins::Other(code) => code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,6 +252,11 @@ impl Ins {
|
|||||||
impl From<u8> for Ins {
|
impl From<u8> for Ins {
|
||||||
fn from(code: u8) -> Self {
|
fn from(code: u8) -> Self {
|
||||||
match code {
|
match code {
|
||||||
|
// Management
|
||||||
|
0x1d => Ins::ReadConfig,
|
||||||
|
0x1c => Ins::WriteConfig,
|
||||||
|
0x1f => Ins::DeviceReset,
|
||||||
|
|
||||||
0x20 => Ins::Verify,
|
0x20 => Ins::Verify,
|
||||||
0x24 => Ins::ChangeReference,
|
0x24 => Ins::ChangeReference,
|
||||||
0x2c => Ins::ResetRetry,
|
0x2c => Ins::ResetRetry,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
//! Miscellaneous constant values
|
//! Miscellaneous constant values
|
||||||
|
|
||||||
/// YubiKey max buffer size
|
/// YubiKey max buffer size
|
||||||
@@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
|
|||||||
// Protected tags
|
// Protected tags
|
||||||
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
||||||
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
||||||
|
|
||||||
|
// Management
|
||||||
|
pub(crate) const TAG_USB_SUPPORTED: u8 = 0x01;
|
||||||
|
pub(crate) const TAG_SERIAL: u8 = 0x02;
|
||||||
|
pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
|
||||||
|
pub(crate) const TAG_FORM_FACTOR: u8 = 0x04;
|
||||||
|
pub(crate) const TAG_VERSION: u8 = 0x05;
|
||||||
|
pub(crate) const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
|
||||||
|
pub(crate) const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
|
||||||
|
pub(crate) const TAG_DEVICE_FLAGS: u8 = 0x08;
|
||||||
|
pub(crate) const TAG_APP_VERSIONS: u8 = 0x09;
|
||||||
|
pub(crate) const TAG_CONFIG_LOCK: u8 = 0x0A;
|
||||||
|
pub(crate) const TAG_UNLOCK: u8 = 0x0B;
|
||||||
|
pub(crate) const TAG_REBOOT: u8 = 0x0C;
|
||||||
|
pub(crate) const TAG_NFC_SUPPORTED: u8 = 0x0D;
|
||||||
|
pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ mod config;
|
|||||||
mod consts;
|
mod consts;
|
||||||
mod error;
|
mod error;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod mgm;
|
pub mod mgm;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
mod mscmap;
|
mod mscmap;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
|
|||||||
+483
-9
@@ -35,20 +35,30 @@ use log::error;
|
|||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
use crate::{
|
|
||||||
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_PROTECTED_MGM},
|
|
||||||
metadata::{AdminData, ProtectedData},
|
|
||||||
piv::{ManagementSlotId, SlotAlgorithmId},
|
|
||||||
transaction::Transaction,
|
|
||||||
yubikey::YubiKey,
|
|
||||||
};
|
|
||||||
use des::{
|
use des::{
|
||||||
cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit},
|
cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit},
|
||||||
TdesEde3,
|
TdesEde3,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use {pbkdf2::pbkdf2_hmac, sha1::Sha1};
|
use {
|
||||||
|
crate::{
|
||||||
|
consts::{
|
||||||
|
CB_BUF_MAX, TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_AUTO_EJECT_TIMEOUT,
|
||||||
|
TAG_CHALRESP_TIMEOUT, TAG_CONFIG_LOCK, TAG_DEVICE_FLAGS, TAG_FORM_FACTOR,
|
||||||
|
TAG_NFC_ENABLED, TAG_NFC_SUPPORTED, TAG_PROTECTED_MGM, TAG_REBOOT, TAG_SERIAL,
|
||||||
|
TAG_UNLOCK, TAG_USB_ENABLED, TAG_USB_SUPPORTED, TAG_VERSION,
|
||||||
|
},
|
||||||
|
metadata::{AdminData, ProtectedData},
|
||||||
|
piv::{ManagementSlotId, SlotAlgorithmId},
|
||||||
|
serialization::Tlv,
|
||||||
|
transaction::Transaction,
|
||||||
|
yubikey::YubiKey,
|
||||||
|
Serial, Version,
|
||||||
|
},
|
||||||
|
bitflags::bitflags,
|
||||||
|
pbkdf2::pbkdf2_hmac,
|
||||||
|
sha1::Sha1,
|
||||||
|
};
|
||||||
|
|
||||||
/// YubiKey MGMT Applet Name
|
/// YubiKey MGMT Applet Name
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
@@ -413,3 +423,467 @@ impl<'a> TryFrom<&'a [u8]> for MgmKey {
|
|||||||
Self::new(key_bytes.try_into().map_err(|_| Error::SizeError)?)
|
Self::new(key_bytes.try_into().map_err(|_| Error::SizeError)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manager for the YubiKey
|
||||||
|
/// Allows to enable applications hosted on the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub struct Manager {
|
||||||
|
client: YubiKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl Manager {
|
||||||
|
/// Open the manager applet on the YubiKey
|
||||||
|
pub fn new(mut client: YubiKey) -> Result<Self> {
|
||||||
|
Transaction::new(&mut client.card)?.select_application(
|
||||||
|
APPLET_ID,
|
||||||
|
APPLET_NAME,
|
||||||
|
"failed selecting YkHSM auth application",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self { client })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable YubiHSM applet
|
||||||
|
pub fn enable_yubihsm(&mut self) -> Result<()> {
|
||||||
|
let mut config = Transaction::new(&mut self.client.card)?.read_config()?;
|
||||||
|
config.config.usb_enabled_apps |= Capability::HSMAUTH;
|
||||||
|
Transaction::new(&mut self.client.card)?.write_config(
|
||||||
|
self.client.version,
|
||||||
|
config.config,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable PIV applet
|
||||||
|
pub fn enable_piv(&mut self) -> Result<()> {
|
||||||
|
let mut config = Transaction::new(&mut self.client.card)?.read_config()?;
|
||||||
|
config.config.usb_enabled_apps |= Capability::PIV;
|
||||||
|
Transaction::new(&mut self.client.card)?.write_config(
|
||||||
|
self.client.version,
|
||||||
|
config.config,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable NFC interface on the device
|
||||||
|
pub fn disable_nfc(&mut self) -> Result<()> {
|
||||||
|
let mut config = Transaction::new(&mut self.client.card)?.read_config()?;
|
||||||
|
config.config.nfc_enabled_apps = Some(Capability::empty());
|
||||||
|
Transaction::new(&mut self.client.card)?.write_config(
|
||||||
|
self.client.version,
|
||||||
|
config.config,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lock configuration of the device
|
||||||
|
pub fn lock_config(
|
||||||
|
&mut self,
|
||||||
|
current_lock: Option<Lock>,
|
||||||
|
new_lock: Option<Lock>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let config = Transaction::new(&mut self.client.card)?.read_config()?;
|
||||||
|
Transaction::new(&mut self.client.card)?.write_config(
|
||||||
|
self.client.version,
|
||||||
|
config.config,
|
||||||
|
current_lock,
|
||||||
|
new_lock,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read configuration from yubikey
|
||||||
|
pub fn read_config(&mut self) -> Result<DeviceInfo> {
|
||||||
|
Transaction::new(&mut self.client.card)?.read_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the inner [`YubiKey`]
|
||||||
|
pub fn into_inner(mut self) -> Result<YubiKey> {
|
||||||
|
Transaction::new(&mut self.client.card)?.select_piv_application()?;
|
||||||
|
Ok(self.client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// secret to lock YubiKey's configuration
|
||||||
|
pub struct Lock(pub [u8; 16]);
|
||||||
|
|
||||||
|
impl Lock {
|
||||||
|
/// Clear the locking of configuration
|
||||||
|
pub const UNLOCKED: Lock = Lock([0; 16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for a device
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub struct DeviceConfig {
|
||||||
|
/// List of applets that are available on the USB interface
|
||||||
|
pub usb_enabled_apps: Capability,
|
||||||
|
/// List of applets that are available on the NFC interface.
|
||||||
|
/// Field will be set to None if the device doesn't support NFC
|
||||||
|
pub nfc_enabled_apps: Option<Capability>,
|
||||||
|
/// When set, the smartcard will automatically eject after the given time.
|
||||||
|
pub auto_eject_timeout: Option<u16>,
|
||||||
|
/// The timeout when waiting for touch for challenge response.
|
||||||
|
pub challenge_response_timeout: Option<u8>,
|
||||||
|
/// Configuration flags
|
||||||
|
pub device_flags: Option<DeviceFlags>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl DeviceConfig {
|
||||||
|
pub(crate) fn as_tlv(
|
||||||
|
&self,
|
||||||
|
reboot: bool,
|
||||||
|
current_lock: Option<Lock>,
|
||||||
|
new_lock: Option<Lock>,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
|
let mut len = data.len();
|
||||||
|
let mut data_remaining = &mut data[1..];
|
||||||
|
|
||||||
|
if reboot {
|
||||||
|
let offset = Tlv::write(data_remaining, TAG_REBOOT, &[])?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lock) = current_lock {
|
||||||
|
let offset = Tlv::write(data_remaining, TAG_UNLOCK, &lock.0[..])?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let offset = Tlv::write(
|
||||||
|
data_remaining,
|
||||||
|
TAG_USB_ENABLED,
|
||||||
|
&self.usb_enabled_apps.bits().to_be_bytes()[..],
|
||||||
|
)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
if let Some(nfc_enabled_apps) = self.nfc_enabled_apps {
|
||||||
|
let offset = Tlv::write(
|
||||||
|
data_remaining,
|
||||||
|
TAG_NFC_ENABLED,
|
||||||
|
&nfc_enabled_apps.bits().to_be_bytes()[..],
|
||||||
|
)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
if let Some(auto_eject_timeout) = self.auto_eject_timeout {
|
||||||
|
let offset = Tlv::write(
|
||||||
|
data_remaining,
|
||||||
|
TAG_AUTO_EJECT_TIMEOUT,
|
||||||
|
&auto_eject_timeout.to_be_bytes()[..],
|
||||||
|
)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
if let Some(challenge_response_timeout) = self.challenge_response_timeout {
|
||||||
|
let offset = Tlv::write(
|
||||||
|
data_remaining,
|
||||||
|
TAG_CHALRESP_TIMEOUT,
|
||||||
|
&challenge_response_timeout.to_be_bytes()[..],
|
||||||
|
)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
if let Some(device_flags) = self.device_flags {
|
||||||
|
let offset = Tlv::write(
|
||||||
|
data_remaining,
|
||||||
|
TAG_DEVICE_FLAGS,
|
||||||
|
&device_flags.bits().to_be_bytes()[..],
|
||||||
|
)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lock) = new_lock {
|
||||||
|
let offset = Tlv::write(data_remaining, TAG_CONFIG_LOCK, &lock.0[..])?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
}
|
||||||
|
|
||||||
|
len -= data_remaining.len();
|
||||||
|
data[0] = (len - 1) as u8;
|
||||||
|
Ok(data[..len].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the NFC interface active
|
||||||
|
pub fn nfc_enabled(&self) -> bool {
|
||||||
|
// It happens that the yubikey will disable the NFC interface entirely if every applet is
|
||||||
|
// disabled.
|
||||||
|
self.nfc_enabled_apps
|
||||||
|
.map(|nfc| !nfc.is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
bitflags! {
|
||||||
|
/// Represents a set of applications.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Capability: u16 {
|
||||||
|
/// One Time Password
|
||||||
|
const OTP =0x01;
|
||||||
|
/// U2F
|
||||||
|
const U2F = 0x02;
|
||||||
|
/// FIDO2
|
||||||
|
const FIDO2 = 0x200;
|
||||||
|
/// OATH
|
||||||
|
const OATH = 0x20;
|
||||||
|
/// PIV
|
||||||
|
const PIV = 0x10;
|
||||||
|
/// OpenPGP
|
||||||
|
const OPENPGP = 0x08;
|
||||||
|
/// HSM Auth
|
||||||
|
const HSMAUTH = 0x100;
|
||||||
|
|
||||||
|
/// General CCID bit
|
||||||
|
const GENERAL_CCID = 0x04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device information
|
||||||
|
/// This represents the configuration and the status of the device
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
/// Configuration of the YubiKey
|
||||||
|
pub config: DeviceConfig,
|
||||||
|
/// Is the configuration locked with a password?
|
||||||
|
pub is_locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl DeviceInfo {
|
||||||
|
pub(crate) fn parse(input: &[u8]) -> Result<Self> {
|
||||||
|
use nom::{
|
||||||
|
bytes::complete::take,
|
||||||
|
combinator::{eof, map},
|
||||||
|
multi::fold_many1,
|
||||||
|
number::complete::{be_u16, be_u32, u8},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn u8_parser(i: &[u8]) -> nom::IResult<&[u8], u8> {
|
||||||
|
let (i, v) = u8(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
fn u16_parser(i: &[u8]) -> nom::IResult<&[u8], u16> {
|
||||||
|
let (i, v) = be_u16(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capability_parser(i: &[u8]) -> nom::IResult<&[u8], Capability> {
|
||||||
|
let (i, v) = map(be_u16, Capability::from_bits_retain)(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
fn serial_parser(i: &[u8]) -> nom::IResult<&[u8], Serial> {
|
||||||
|
let (i, v) = map(be_u32, Serial)(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Info {
|
||||||
|
usb_supported_apps: Option<Capability>,
|
||||||
|
usb_enabled_apps: Option<Capability>,
|
||||||
|
nfc_supported_apps: Option<Capability>,
|
||||||
|
nfc_enabled_apps: Option<Capability>,
|
||||||
|
serial: Option<Serial>,
|
||||||
|
form_factor: Option<FormFactor>,
|
||||||
|
version: Option<Version>,
|
||||||
|
auto_eject_timeout: Option<u16>,
|
||||||
|
challenge_response_timeout: Option<u8>,
|
||||||
|
device_flags: Option<DeviceFlags>,
|
||||||
|
is_locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (input, len) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (input, rest) = take(len)(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (_, _) = eof(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
|
||||||
|
let out = fold_many1(
|
||||||
|
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|
||||||
|
|| Ok(Info::default()),
|
||||||
|
|acc: Result<Info>, tlv| match acc {
|
||||||
|
Ok(mut config) => {
|
||||||
|
match tlv.tag {
|
||||||
|
v if v == TAG_USB_SUPPORTED => {
|
||||||
|
config.usb_supported_apps = Some(
|
||||||
|
capability_parser(tlv.value)
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.1,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_USB_ENABLED => {
|
||||||
|
config.usb_enabled_apps = Some(
|
||||||
|
capability_parser(tlv.value)
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.1,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_NFC_SUPPORTED => {
|
||||||
|
config.nfc_supported_apps = Some(
|
||||||
|
capability_parser(tlv.value)
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.1,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_NFC_ENABLED => {
|
||||||
|
config.nfc_enabled_apps = Some(
|
||||||
|
capability_parser(tlv.value)
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.1,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_SERIAL => {
|
||||||
|
config.serial =
|
||||||
|
Some(serial_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_FORM_FACTOR => {
|
||||||
|
config.form_factor = Some(FormFactor::parse(tlv.value)?);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_VERSION => {
|
||||||
|
config.version = Some(Version::parse(tlv.value)?);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_AUTO_EJECT_TIMEOUT => {
|
||||||
|
config.auto_eject_timeout =
|
||||||
|
Some(u16_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_CHALRESP_TIMEOUT => {
|
||||||
|
config.challenge_response_timeout =
|
||||||
|
Some(u8_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_DEVICE_FLAGS => {
|
||||||
|
config.device_flags = Some(
|
||||||
|
DeviceFlags::parse(tlv.value)
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.1,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
v if v == TAG_CONFIG_LOCK => {
|
||||||
|
config.is_locked = tlv.value == b"\x01";
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
// TODO(baloo): implement config lock
|
||||||
|
_unsupported => {
|
||||||
|
// New unsupported tags
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err => err,
|
||||||
|
},
|
||||||
|
)(rest)
|
||||||
|
.map_err(|_: nom::Err<()>| Error::ParseError)?
|
||||||
|
.1?;
|
||||||
|
|
||||||
|
let Some(usb_enabled_apps) = out.usb_enabled_apps else {
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = DeviceConfig {
|
||||||
|
usb_enabled_apps,
|
||||||
|
nfc_enabled_apps: out.nfc_enabled_apps,
|
||||||
|
auto_eject_timeout: out.auto_eject_timeout,
|
||||||
|
challenge_response_timeout: out.challenge_response_timeout,
|
||||||
|
device_flags: out.device_flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DeviceInfo {
|
||||||
|
config,
|
||||||
|
is_locked: out.is_locked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FormFactor of the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum FormFactor {
|
||||||
|
/// YubiKey reported an unknown form factor
|
||||||
|
Unknown = 0x00,
|
||||||
|
/// Full size USB-A YubiKey
|
||||||
|
UsbAKeychain = 0x01,
|
||||||
|
/// Nano USB-A YubiKey
|
||||||
|
UsbANano = 0x02,
|
||||||
|
/// Full size USB-C YubiKey
|
||||||
|
UsbCKeychain = 0x03,
|
||||||
|
/// Nano USB-C YubiKey
|
||||||
|
UsbCNano = 0x04,
|
||||||
|
/// Lightning // USB-C YubiKey
|
||||||
|
UsbCLightning = 0x05,
|
||||||
|
/// USB-A YubiKey with a fingerprint reader
|
||||||
|
UsbABio = 0x06,
|
||||||
|
/// USB-C YubiKey with a fingerprint reader
|
||||||
|
UsbCBio = 0x07,
|
||||||
|
/// Unsupported form factor
|
||||||
|
Unsupported(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl FormFactor {
|
||||||
|
fn parse(input: &[u8]) -> Result<Self> {
|
||||||
|
use nom::{combinator::eof, number::complete::u8};
|
||||||
|
|
||||||
|
let (i, v) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
|
||||||
|
Ok(match v {
|
||||||
|
0 => Self::Unknown,
|
||||||
|
1 => Self::UsbAKeychain,
|
||||||
|
2 => Self::UsbANano,
|
||||||
|
3 => Self::UsbCKeychain,
|
||||||
|
4 => Self::UsbCNano,
|
||||||
|
5 => Self::UsbCLightning,
|
||||||
|
6 => Self::UsbABio,
|
||||||
|
7 => Self::UsbCBio,
|
||||||
|
v => Self::Unsupported(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
bitflags! {
|
||||||
|
/// Represents configuration flags.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DeviceFlags: u8{
|
||||||
|
/// Remote wake-up
|
||||||
|
const REMOTE_WAKEUP = 0x40;
|
||||||
|
/// Eject
|
||||||
|
const Eject = 0x80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl DeviceFlags {
|
||||||
|
fn parse(i: &[u8]) -> nom::IResult<&[u8], Self> {
|
||||||
|
use nom::{
|
||||||
|
combinator::{eof, map},
|
||||||
|
number::complete::u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (i, v) = map(u8, Self::from_bits_retain)(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+91
-40
@@ -15,7 +15,7 @@ use log::{error, trace};
|
|||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use crate::mgm::{MgmKey, DES_LEN_3DES};
|
use crate::mgm::{DeviceConfig, DeviceInfo, Lock, MgmKey, DES_LEN_3DES};
|
||||||
|
|
||||||
const CB_PIN_MAX: usize = 8;
|
const CB_PIN_MAX: usize = 8;
|
||||||
|
|
||||||
@@ -60,23 +60,32 @@ impl<'tx> Transaction<'tx> {
|
|||||||
Ok(recv_buffer)
|
Ok(recv_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select PIV application.
|
||||||
|
pub fn select_piv_application(&self) -> Result<()> {
|
||||||
|
self.select_application(
|
||||||
|
piv::APPLET_ID,
|
||||||
|
piv::APPLET_NAME,
|
||||||
|
"failed selecting application",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Select application.
|
/// Select application.
|
||||||
pub fn select_application(&self) -> Result<()> {
|
pub fn select_application(
|
||||||
|
&self,
|
||||||
|
applet: &[u8],
|
||||||
|
applet_name: &'static str,
|
||||||
|
error: &'static str,
|
||||||
|
) -> Result<()> {
|
||||||
let response = Apdu::new(Ins::SelectApplication)
|
let response = Apdu::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(piv::APPLET_ID)
|
.data(applet)
|
||||||
.transmit(self, 0xFF)
|
.transmit(self, 0xFF)
|
||||||
.inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
|
.inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
error!(
|
error!("{}: {:04x}", error, response.status_words().code());
|
||||||
"failed selecting application: {:04x}",
|
|
||||||
response.status_words().code()
|
|
||||||
);
|
|
||||||
return Err(match response.status_words() {
|
return Err(match response.status_words() {
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
|
||||||
applet_name: piv::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
_ => Error::GenericError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,21 +110,11 @@ impl<'tx> Transaction<'tx> {
|
|||||||
match version.major {
|
match version.major {
|
||||||
// YK4 requires switching to the YK applet to retrieve the serial
|
// YK4 requires switching to the YK applet to retrieve the serial
|
||||||
4 => {
|
4 => {
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
self.select_application(
|
||||||
.p1(0x04)
|
otp::APPLET_ID,
|
||||||
.data(otp::APPLET_ID)
|
otp::APPLET_NAME,
|
||||||
.transmit(self, 0xFF)?
|
"failed selecting yk application",
|
||||||
.status_words();
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
|
||||||
error!("failed selecting yk application: {:04x}", sw.code());
|
|
||||||
return Err(match sw {
|
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
|
||||||
applet_name: otp::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
@@ -129,21 +128,11 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reselect the PIV applet
|
// reselect the PIV applet
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
self.select_application(
|
||||||
.p1(0x04)
|
piv::APPLET_ID,
|
||||||
.data(piv::APPLET_ID)
|
piv::APPLET_NAME,
|
||||||
.transmit(self, 0xFF)?
|
"failed selecting application",
|
||||||
.status_words();
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
|
||||||
error!("failed selecting application: {:04x}", sw.code());
|
|
||||||
return Err(match sw {
|
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
|
||||||
applet_name: piv::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
response.data().try_into()
|
response.data().try_into()
|
||||||
}
|
}
|
||||||
@@ -524,4 +513,66 @@ impl<'tx> Transaction<'tx> {
|
|||||||
_ => Err(Error::GenericError),
|
_ => Err(Error::GenericError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write configuration to the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn write_config(
|
||||||
|
&mut self,
|
||||||
|
version: Version,
|
||||||
|
config: DeviceConfig,
|
||||||
|
current_lock: Option<Lock>,
|
||||||
|
new_lock: Option<Lock>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if version
|
||||||
|
< (Version {
|
||||||
|
major: 5,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(Error::NotSupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = config.as_tlv(true, current_lock, new_lock)?;
|
||||||
|
|
||||||
|
let response = Apdu::new(Ins::WriteConfig)
|
||||||
|
.params(0x00, 0x00)
|
||||||
|
.data(&data)
|
||||||
|
.transmit(self, 2)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!(
|
||||||
|
"Unable to write_config: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write configuration to the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn read_config(&mut self) -> Result<DeviceInfo> {
|
||||||
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
|
let mut len = data.len();
|
||||||
|
let data_remaining = &mut data[..];
|
||||||
|
|
||||||
|
len -= data_remaining.len();
|
||||||
|
let response = Apdu::new(Ins::ReadConfig)
|
||||||
|
.params(0x00, 0x00)
|
||||||
|
.data(&data[..len])
|
||||||
|
.transmit(self, CB_BUF_MAX + 2)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!(
|
||||||
|
"Unable to read configuration: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = response.data();
|
||||||
|
DeviceInfo::parse(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-3
@@ -45,6 +45,7 @@ use log::{error, info};
|
|||||||
use pcsc::Card;
|
use pcsc::Card;
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::{Ord, Ordering},
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
@@ -143,6 +144,22 @@ impl Version {
|
|||||||
patch: bytes[2],
|
patch: bytes[2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub(crate) fn parse(input: &[u8]) -> Result<Self> {
|
||||||
|
use nom::{combinator::eof, number::complete::u8};
|
||||||
|
|
||||||
|
let (i, major) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (i, minor) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (i, patch) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Version {
|
impl Display for Version {
|
||||||
@@ -151,6 +168,39 @@ impl Display for Version {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ord for Version {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.major > other.major {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.major < other.major {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.minor > other.minor {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.minor < other.minor {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.patch > other.patch {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.patch < other.patch {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Version {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// YubiKey device: primary API for opening a session and performing various operations.
|
/// YubiKey device: primary API for opening a session and performing various operations.
|
||||||
///
|
///
|
||||||
/// Almost all functionality in this library will require an open session
|
/// Almost all functionality in this library will require an open session
|
||||||
@@ -275,7 +325,7 @@ impl YubiKey {
|
|||||||
.map(|p| Buffer::new(p.expose_secret().clone()));
|
.map(|p| Buffer::new(p.expose_secret().clone()));
|
||||||
|
|
||||||
let txn = Transaction::new(&mut self.card)?;
|
let txn = Transaction::new(&mut self.card)?;
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
if let Some(p) = &pin {
|
if let Some(p) = &pin {
|
||||||
txn.verify_pin(p)?;
|
txn.verify_pin(p)?;
|
||||||
@@ -462,7 +512,7 @@ impl YubiKey {
|
|||||||
// Force a re-select to unverify, because once verified the spec dictates that
|
// Force a re-select to unverify, because once verified the spec dictates that
|
||||||
// subsequent verify calls will return a "verification not needed" instead of
|
// subsequent verify calls will return a "verification not needed" instead of
|
||||||
// the number of tries left...
|
// the number of tries left...
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
// WRONG_PIN is expected on successful query.
|
// WRONG_PIN is expected on successful query.
|
||||||
match txn.verify_pin(&[]) {
|
match txn.verify_pin(&[]) {
|
||||||
@@ -708,7 +758,7 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
|
|||||||
|
|
||||||
let mut app_version_serial = || -> Result<(Version, Serial)> {
|
let mut app_version_serial = || -> Result<(Version, Serial)> {
|
||||||
let txn = Transaction::new(&mut card)?;
|
let txn = Transaction::new(&mut card)?;
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
let v = txn.get_version()?;
|
let v = txn.get_version()?;
|
||||||
let s = txn.get_serial(v)?;
|
let s = txn.get_serial(v)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user