diff --git a/Cargo.lock b/Cargo.lock index f1d542e..63d0ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,9 +85,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -739,7 +739,7 @@ version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1051,6 +1051,7 @@ name = "yubikey" version = "0.8.0" dependencies = [ "base16ct", + "bitflags 2.5.0", "der", "des", "ecdsa", diff --git a/Cargo.toml b/Cargo.toml index ec5d52b..8951796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [".", "cli"] x509-cert = { version = "0.2.5", features = [ "builder", "hazmat" ] } [dependencies] +bitflags = "2.5.0" der = "0.7.1" des = "0.8" elliptic-curve = "0.13" diff --git a/src/apdu.rs b/src/apdu.rs index d8942a3..227f7dc 100644 --- a/src/apdu.rs +++ b/src/apdu.rs @@ -198,6 +198,15 @@ pub enum Ins { /// Get slot metadata GetMetadata, + /// Management // Read Config + ReadConfig, + + /// Management // Write Config + WriteConfig, + + /// Management // DeviceReset + DeviceReset, + /// Other/unrecognized instruction codes Other(u8), } @@ -223,6 +232,12 @@ impl Ins { Ins::Attest => 0xf9, Ins::GetSerial => 0xf8, Ins::GetMetadata => 0xf7, + + // Management + Ins::ReadConfig => 0x1d, + Ins::WriteConfig => 0x1c, + Ins::DeviceReset => 0x1f, + Ins::Other(code) => code, } } @@ -231,6 +246,11 @@ impl Ins { impl From for Ins { fn from(code: u8) -> Self { match code { + // Management + 0x1d => Ins::ReadConfig, + 0x1c => Ins::WriteConfig, + 0x1f => Ins::DeviceReset, + 0x20 => Ins::Verify, 0x24 => Ins::ChangeReference, 0x2c => Ins::ResetRetry, diff --git a/src/consts.rs b/src/consts.rs index 03242ab..bc40a7d 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Miscellaneous constant values /// YubiKey max buffer size @@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83; // Protected tags pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81; 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; diff --git a/src/lib.rs b/src/lib.rs index 74cdf8c..a5bb5ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ mod config; mod consts; mod error; mod metadata; -mod mgm; +pub mod mgm; #[cfg(feature = "untested")] mod mscmap; #[cfg(feature = "untested")] diff --git a/src/mgm.rs b/src/mgm.rs index c8f4191..a31a523 100644 --- a/src/mgm.rs +++ b/src/mgm.rs @@ -35,18 +35,29 @@ use log::error; use rand_core::{OsRng, RngCore}; use zeroize::{Zeroize, Zeroizing}; -#[cfg(feature = "untested")] -use crate::{ - consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_PROTECTED_MGM}, - metadata::{AdminData, ProtectedData}, - yubikey::YubiKey, -}; use des::{ cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit}, TdesEde3, }; #[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_DEVICE_FLAGS, TAG_FORM_FACTOR, TAG_NFC_ENABLED, + TAG_NFC_SUPPORTED, TAG_PROTECTED_MGM, TAG_REBOOT, TAG_SERIAL, TAG_USB_ENABLED, + TAG_USB_SUPPORTED, TAG_VERSION, + }, + metadata::{AdminData, ProtectedData}, + serialization::Tlv, + transaction::Transaction, + yubikey::YubiKey, + Serial, Version, + }, + bitflags::bitflags, + pbkdf2::pbkdf2_hmac, + sha1::Sha1, +}; /// YubiKey MGMT Applet Name #[cfg(feature = "untested")] @@ -427,3 +438,371 @@ fn is_weak_key(key: &[u8; DES_LEN_3DES]) -> bool { is_weak } + +/// 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 { + 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)?; + Ok(()) + } + + /// Return the inner [`YubiKey`] + pub fn into_inner(mut self) -> Result { + Transaction::new(&mut self.client.card)?.select_piv_application()?; + Ok(self.client) + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg(feature = "untested")] +pub(crate) struct DeviceConfig { + usb_enabled_apps: Capability, + nfc_enabled_apps: Capability, + auto_eject_timeout: Option, + challenge_response_timeout: Option, + device_flags: Option, +} + +#[cfg(feature = "untested")] +impl DeviceConfig { + pub(crate) fn as_tlv(&self, reboot: bool) -> Result> { + 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 !self.usb_enabled_apps.is_empty() { + let offset = Tlv::write( + data_remaining, + TAG_USB_ENABLED, + &self.usb_enabled_apps.bits().to_be_bytes()[..], + )?; + data_remaining = &mut data_remaining[offset..]; + } + if !self.nfc_enabled_apps.is_empty() { + let offset = Tlv::write( + data_remaining, + TAG_NFC_ENABLED, + &self.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..]; + } + + len -= data_remaining.len(); + data[0] = (len - 1) as u8; + Ok((&data[..len]).to_vec()) + } +} + +#[cfg(feature = "untested")] +bitflags! { + /// Represents a set of applications. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + 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; + } +} + +#[cfg(feature = "untested")] +pub(crate) struct DeviceInfo { + pub(crate) config: DeviceConfig, +} + +#[cfg(feature = "untested")] +impl DeviceInfo { + pub(crate) fn parse(input: &[u8]) -> Result { + 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, + usb_enabled_apps: Option, + nfc_supported_apps: Option, + nfc_enabled_apps: Option, + serial: Option, + form_factor: Option, + version: Option, + auto_eject_timeout: Option, + challenge_response_timeout: Option, + device_flags: Option, + } + + 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, 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) + } + // TODO(baloo): implement config lock + _unsupported => { + // New unsupported tags + Ok(config) + } + } + } + err => err, + }, + )(rest) + .map_err(|_: nom::Err<()>| Error::ParseError)? + .1?; + + let usb_enabled_apps = if let Some(enabled) = out.usb_enabled_apps { + enabled + } else { + return Err(Error::ParseError); + }; + let nfc_enabled_apps = if let Some(enabled) = out.nfc_enabled_apps { + enabled + } else { + return Err(Error::ParseError); + }; + + let config = DeviceConfig { + usb_enabled_apps, + 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 }) + } +} + +/// 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 { + 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)] + 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)) + } +} diff --git a/src/transaction.rs b/src/transaction.rs index 65140ad..1304863 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -15,7 +15,7 @@ use log::{error, trace}; use zeroize::Zeroizing; #[cfg(feature = "untested")] -use crate::mgm::{MgmKey, DES_LEN_3DES}; +use crate::mgm::{DeviceConfig, DeviceInfo, MgmKey, DES_LEN_3DES}; const CB_PIN_MAX: usize = 8; @@ -60,11 +60,25 @@ impl<'tx> Transaction<'tx> { 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. - 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) .p1(0x04) - .data(piv::APPLET_ID) + .data(applet) .transmit(self, 0xFF) .map_err(|e| { error!("failed communicating with card: '{}'", e); @@ -72,14 +86,9 @@ impl<'tx> Transaction<'tx> { })?; if !response.is_success() { - error!( - "failed selecting application: {:04x}", - response.status_words().code() - ); + error!("{}: {:04x}", error, response.status_words().code()); return Err(match response.status_words() { - StatusWords::NotFoundError => Error::AppletNotFound { - applet_name: piv::APPLET_NAME, - }, + StatusWords::NotFoundError => Error::AppletNotFound { applet_name }, _ => Error::GenericError, }); } @@ -104,21 +113,11 @@ impl<'tx> Transaction<'tx> { match version.major { // YK4 requires switching to the YK applet to retrieve the serial 4 => { - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(otp::APPLET_ID) - .transmit(self, 0xFF)? - .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, - }); - } + self.select_application( + otp::APPLET_ID, + otp::APPLET_NAME, + "failed selecting yk application", + )?; let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?; @@ -132,21 +131,11 @@ impl<'tx> Transaction<'tx> { } // reselect the PIV applet - let sw = Apdu::new(Ins::SelectApplication) - .p1(0x04) - .data(piv::APPLET_ID) - .transmit(self, 0xFF)? - .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, - }); - } + self.select_application( + piv::APPLET_ID, + piv::APPLET_NAME, + "failed selecting application", + )?; response.data().try_into() } @@ -511,4 +500,60 @@ impl<'tx> Transaction<'tx> { _ => Err(Error::GenericError), } } + + /// Write configuration to the YubiKey + #[cfg(feature = "untested")] + pub fn write_config(&mut self, version: Version, config: DeviceConfig) -> Result<()> { + if version + < (Version { + major: 5, + minor: 0, + patch: 0, + }) + { + return Err(Error::NotSupported); + } + + let data = config.as_tlv(true)?; + + 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 { + 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) + } } diff --git a/src/yubikey.rs b/src/yubikey.rs index fb62bc4..2fefa94 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -45,6 +45,7 @@ use log::{error, info}; use pcsc::{Card, Disposition}; use rand_core::{OsRng, RngCore}; use std::{ + cmp::{Ord, Ordering}, fmt::{self, Display}, str::FromStr, }; @@ -143,6 +144,22 @@ impl Version { patch: bytes[2], } } + + #[cfg(feature = "untested")] + pub(crate) fn parse(input: &[u8]) -> Result { + 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 { @@ -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 { + Some(self.cmp(other)) + } +} + /// YubiKey device: primary API for opening a session and performing various operations. /// /// 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())); let txn = Transaction::new(&mut self.card)?; - txn.select_application()?; + txn.select_piv_application()?; if let Some(p) = &pin { txn.verify_pin(p)?; @@ -459,7 +509,7 @@ impl YubiKey { // Force a re-select to unverify, because once verified the spec dictates that // subsequent verify calls will return a "verification not needed" instead of // the number of tries left... - txn.select_application()?; + txn.select_piv_application()?; // WRONG_PIN is expected on successful query. match txn.verify_pin(&[]) { @@ -712,7 +762,7 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey { let mut app_version_serial = || -> Result<(Version, Serial)> { let txn = Transaction::new(&mut card)?; - txn.select_application()?; + txn.select_piv_application()?; let v = txn.get_version()?; let s = txn.get_serial(v)?;