use crate::piv::mgmt::{
parse_retries_remaining, GET_METADATA_MGMT, GET_METADATA_PIN, GET_METADATA_PUK,
};
use crate::piv::{MgmtAlgo, PivBackend};
use anyhow::{bail, Result};
use std::collections::HashMap;
use zeroize::Zeroizing;
pub const OBJ_ADMIN_DATA: u32 = 0x5F_FF00;
pub const OBJ_PRINTED: u32 = 0x5F_C109;
pub const DEFAULT_PIN: &str = "123456";
pub const DEFAULT_PUK: &str = "12345678";
pub const DEFAULT_MANAGEMENT_KEY: &str = "010203040506070801020304050607080102030405060708";
const TAG_IS_DEFAULT: u8 = 0x05;
pub(crate) fn parse_tlv_flat(data: &[u8]) -> HashMap<u8, Vec<u8>> {
let mut map = HashMap::new();
let mut i = 0;
while i < data.len() {
let tag = data[i];
i += 1;
if i >= data.len() {
break;
}
let (len, consumed) = decode_tlv_length(&data[i..]);
i += consumed;
if i + len > data.len() {
debug_assert!(
false,
"parse_tlv_flat: truncated TLV at offset {i} (tag=0x{tag:02x}, claimed len={len}, available={})",
data.len() - i
);
break;
}
map.insert(tag, data[i..i + len].to_vec());
i += len;
}
map
}
pub(crate) fn decode_tlv_length(data: &[u8]) -> (usize, usize) {
if data.is_empty() {
return (0, 0);
}
if data[0] & 0x80 == 0 {
(data[0] as usize, 1)
} else {
let n = (data[0] & 0x7f) as usize;
if data.len() < 1 + n {
return (0, 1);
}
let mut len = 0usize;
for b in &data[1..1 + n] {
len = (len << 8) | (*b as usize);
}
(len, 1 + n)
}
}
const TAG_PRINTED: u8 = 0x88;
const TAG_PRINTED_KEY: u8 = 0x89;
const TAG_PRINTED_PREVIOUS_KEY: u8 = 0x8A;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PrintedKeys {
pub current: Option<Zeroizing<String>>,
pub previous: Option<Zeroizing<String>>,
}
impl PrintedKeys {
pub fn parse(raw: &[u8]) -> Result<Self> {
let mut keys = Self::default();
if raw.first() != Some(&TAG_PRINTED) {
return Ok(keys);
}
let zeroizing = |list: Vec<(u8, Vec<u8>)>| -> Vec<(u8, Zeroizing<Vec<u8>>)> {
list.into_iter()
.map(|(t, v)| (t, Zeroizing::new(v)))
.collect()
};
let outer = zeroizing(parse_tlv_list(raw)?);
let Some((_, inner)) = outer.first() else {
return Ok(keys);
};
for (tag, value) in zeroizing(parse_tlv_list(inner)?) {
let key = || Some(Zeroizing::new(hex::encode(&*value)));
match tag {
TAG_PRINTED_KEY => keys.current = key(),
TAG_PRINTED_PREVIOUS_KEY => keys.previous = key(),
_ => {}
}
}
Ok(keys)
}
}
pub fn encode_printed(current_hex: &str, previous_hex: Option<&str>) -> Result<Zeroizing<Vec<u8>>> {
use crate::piv::tlv::encode_tlv;
let key = |h: &str| -> Result<Zeroizing<Vec<u8>>> {
Ok(Zeroizing::new(
hex::decode(h).map_err(|e| anyhow::anyhow!("management key hex: {e}"))?,
))
};
let mut inner = Zeroizing::new(encode_tlv(TAG_PRINTED_KEY, &key(current_hex)?));
if let Some(p) = previous_hex {
inner.extend(encode_tlv(TAG_PRINTED_PREVIOUS_KEY, &key(p)?));
}
Ok(Zeroizing::new(encode_tlv(TAG_PRINTED, &inner)))
}
pub fn read_printed_keys(reader: &str, piv: &dyn PivBackend, pin: &str) -> Result<PrintedKeys> {
match piv.read_printed_object_with_pin(reader, pin) {
Ok(raw) => PrintedKeys::parse(&Zeroizing::new(raw)),
Err(_) => Ok(PrintedKeys::default()),
}
}
pub(crate) fn parse_subject_dn(subject: &str) -> rcgen::DistinguishedName {
use rcgen::{DistinguishedName, DnType};
let mut dn = DistinguishedName::new();
for part in subject.split('/').filter(|s| !s.is_empty()) {
if let Some((k, v)) = part.split_once('=') {
match k.trim() {
"CN" => dn.push(DnType::CommonName, v.trim()),
"O" => dn.push(DnType::OrganizationName, v.trim()),
"OU" => dn.push(DnType::OrganizationalUnitName, v.trim()),
_ => {}
}
}
}
dn
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DefaultCredentials {
pub pin: bool,
pub puk: bool,
pub management_key: bool,
}
impl DefaultCredentials {
pub fn any(&self) -> bool {
self.pin || self.puk || self.management_key
}
}
pub fn detect_default_credentials(reader: &str, piv: &dyn PivBackend) -> DefaultCredentials {
let is_default = |apdu: &[u8]| -> Option<bool> {
let resp = piv.send_apdu(reader, apdu).ok()?;
Some(
parse_tlv_flat(&resp)
.get(&TAG_IS_DEFAULT)
.map(Vec::as_slice)
== Some(&[0x01]),
)
};
match (
is_default(&GET_METADATA_PIN),
is_default(&GET_METADATA_PUK),
is_default(&GET_METADATA_MGMT),
) {
(Some(pin), Some(puk), Some(management_key)) => DefaultCredentials {
pin,
puk,
management_key,
},
_ => DefaultCredentials::default(),
}
}
pub const ADMIN_FLAG_PUK_BLOCKED: u8 = 0x01;
pub const ADMIN_FLAG_MGMT_KEY_STORED: u8 = 0x02;
const TAG_ADMIN_DATA: u8 = 0x80;
const TAG_ADMIN_FLAGS: u8 = 0x81;
const TAG_ADMIN_SALT: u8 = 0x82;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtectionMode {
None,
Standard,
LegacyOrPukBlocked,
Derived,
Invalid,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AdminData {
pub flags: Option<u8>,
pub salt: Option<Vec<u8>>,
pub other: Vec<(u8, Vec<u8>)>,
}
impl AdminData {
pub fn parse(raw: &[u8]) -> Result<Self> {
let mut admin = Self::default();
if raw.is_empty() {
return Ok(admin);
}
let inner = match parse_tlv_list(raw)?.into_iter().next() {
Some((TAG_ADMIN_DATA, inner)) => inner,
_ => bail!("ADMIN DATA: expected tag 0x80"),
};
for (tag, value) in parse_tlv_list(&inner)? {
match tag {
TAG_ADMIN_FLAGS => match value.as_slice() {
[flags] => admin.flags = Some(*flags),
_ => bail!("ADMIN DATA: flags (tag 0x81) must be one byte"),
},
TAG_ADMIN_SALT => admin.salt = Some(value),
_ => admin.other.push((tag, value)),
}
}
Ok(admin)
}
pub fn to_bytes(&self) -> Vec<u8> {
use crate::piv::tlv::encode_tlv;
let mut inner = Vec::new();
if let Some(flags) = self.flags {
inner.extend(encode_tlv(TAG_ADMIN_FLAGS, &[flags]));
}
if let Some(ref salt) = self.salt {
inner.extend(encode_tlv(TAG_ADMIN_SALT, salt));
}
for (tag, value) in &self.other {
inner.extend(encode_tlv(*tag, value));
}
encode_tlv(TAG_ADMIN_DATA, &inner)
}
pub fn protection_mode(&self) -> ProtectionMode {
let flags = self.flags.unwrap_or(0);
if self.salt.is_some() {
ProtectionMode::Derived
} else if flags & ADMIN_FLAG_MGMT_KEY_STORED != 0 {
ProtectionMode::Standard
} else if flags & ADMIN_FLAG_PUK_BLOCKED != 0 {
ProtectionMode::LegacyOrPukBlocked
} else {
ProtectionMode::None
}
}
}
fn parse_tlv_list(data: &[u8]) -> Result<Vec<(u8, Vec<u8>)>> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let tag = data[i];
i += 1;
if i >= data.len() {
bail!("truncated TLV: tag 0x{tag:02x} has no length");
}
let (len, consumed) = decode_tlv_length(&data[i..]);
i += consumed;
if consumed == 0 || i + len > data.len() {
bail!("truncated TLV: tag 0x{tag:02x}");
}
out.push((tag, data[i..i + len].to_vec()));
i += len;
}
Ok(out)
}
pub fn read_admin_data(reader: &str, piv: &dyn PivBackend) -> Result<AdminData> {
match piv.read_object(reader, OBJ_ADMIN_DATA) {
Err(_) => Ok(AdminData::default()),
Ok(raw) => AdminData::parse(&raw),
}
}
pub fn detect_protection_mode(reader: &str, piv: &dyn PivBackend) -> ProtectionMode {
match read_admin_data(reader, piv) {
Ok(admin) => admin.protection_mode(),
Err(_) => ProtectionMode::Invalid,
}
}
fn puk_retries_remaining(reader: &str, piv: &dyn PivBackend) -> Option<u8> {
let resp = piv.send_apdu(reader, &GET_METADATA_PUK).ok()?;
parse_retries_remaining(&resp)
}
pub fn admin_data_with_stored_key(
reader: &str,
piv: &dyn PivBackend,
clearing_legacy_flag: bool,
) -> Result<Vec<u8>> {
let mut admin = read_admin_data(reader, piv)
.map_err(|e| anyhow::anyhow!("cannot update ADMIN DATA (0x5FFF00): {e}"))?;
if admin.salt.is_some() {
return Err(pin_derived_unsupported().into());
}
let mut flags = admin.flags.unwrap_or(0) | ADMIN_FLAG_MGMT_KEY_STORED;
match puk_retries_remaining(reader, piv) {
Some(0) => flags |= ADMIN_FLAG_PUK_BLOCKED,
Some(_) => flags &= !ADMIN_FLAG_PUK_BLOCKED,
None if clearing_legacy_flag => flags &= !ADMIN_FLAG_PUK_BLOCKED,
None => {}
}
admin.flags = Some(flags);
Ok(admin.to_bytes())
}
pub fn pin_derived_unsupported() -> crate::errors::YbError {
crate::errors::YbError::new("this YubiKey's management key is PIN-derived")
.why("that mode is deprecated, and yb does not support it")
.fix(
"have the YubiKey keep a random management key instead, unlocked by the PIN: \
`ykman piv access change-management-key --generate --protect`",
)
}
pub fn generate_random_management_key(algo: MgmtAlgo) -> Zeroizing<String> {
let bytes: Zeroizing<Vec<u8>> =
Zeroizing::new((0..algo.key_len()).map(|_| rand::random::<u8>()).collect());
Zeroizing::new(hex::encode(&*bytes))
}
pub struct KeySwitch<'a> {
pub old_key: &'a str,
pub old_key_in_printed: bool,
pub new_key: &'a str,
pub algo: MgmtAlgo,
pub clearing_legacy_flag: bool,
}
pub fn enable_pin_protected_management_key(
reader: &str,
piv: &dyn PivBackend,
sw: &KeySwitch<'_>,
) -> Result<()> {
use anyhow::Context as _;
let _lock = piv.exclusive(reader)?;
let admin_payload = admin_data_with_stored_key(reader, piv, sw.clearing_legacy_flag)?;
let both = encode_printed(sw.new_key, Some(sw.old_key))?;
if let Err(e) = piv.write_object(reader, OBJ_PRINTED, &both, sw.old_key) {
return Err(e).context(restored_or_not(
reader,
piv,
sw,
"saving the new management key in PRINTED failed",
));
}
if let Err(e) = piv.set_management_key(reader, sw.old_key, sw.new_key, sw.algo) {
if piv.authenticate_management_key(reader, sw.old_key).is_ok() {
return Err(e).context(restored_or_not(
reader,
piv,
sw,
"the YubiKey rejected the new management key",
));
}
if piv.authenticate_management_key(reader, sw.new_key).is_err() {
return Err(e).context(
"the management key switch was interrupted and its outcome is unknown \
(was the YubiKey removed?); reconnect the YubiKey",
);
}
}
piv.write_object(reader, OBJ_ADMIN_DATA, &admin_payload, sw.new_key)
.context(
"the management key was changed and saved in PRINTED, but ADMIN DATA could \
not be updated",
)?;
let _ = encode_printed(sw.new_key, None)
.and_then(|only_new| piv.write_object(reader, OBJ_PRINTED, &only_new, sw.new_key));
Ok(())
}
fn restored_or_not(reader: &str, piv: &dyn PivBackend, sw: &KeySwitch<'_>, what: &str) -> String {
let previous = if sw.old_key_in_printed {
encode_printed(sw.old_key, None)
} else {
Ok(Zeroizing::new(Vec::new()))
};
let restored = previous
.and_then(|data| piv.write_object(reader, OBJ_PRINTED, &data, sw.old_key))
.is_ok();
if restored {
format!("{what}; nothing was changed")
} else {
format!("{what}; the management key is unchanged")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tlv_simple() {
let data = [0x05u8, 0x01, 0x01];
let map = parse_tlv_flat(&data);
assert_eq!(map.get(&0x05), Some(&vec![0x01u8]));
}
#[test]
fn tlv_multi_tag() {
let data = [0x05u8, 0x01, 0x00, 0x06, 0x02, 0x03, 0x04];
let map = parse_tlv_flat(&data);
assert_eq!(map.get(&0x05), Some(&vec![0x00u8]));
assert_eq!(map.get(&0x06), Some(&vec![0x03u8, 0x04u8]));
}
#[test]
fn tlv_long_form_length() {
let data = [0x01u8, 0x81, 0x03, 0xAA, 0xBB, 0xCC];
let map = parse_tlv_flat(&data);
assert_eq!(map.get(&0x01), Some(&vec![0xAAu8, 0xBB, 0xCC]));
}
fn mode(raw: &[u8]) -> ProtectionMode {
AdminData::parse(raw).unwrap().protection_mode()
}
#[test]
fn admin_data_protection_modes() {
assert_eq!(mode(&[]), ProtectionMode::None);
assert_eq!(mode(&[0x80, 0x03, 0x81, 0x01, 0x00]), ProtectionMode::None);
assert_eq!(
mode(&[0x80, 0x03, 0x81, 0x01, 0x02]),
ProtectionMode::Standard
);
assert_eq!(
mode(&[0x80, 0x03, 0x81, 0x01, 0x03]),
ProtectionMode::Standard
);
assert_eq!(
mode(&[0x80, 0x03, 0x81, 0x01, 0x01]),
ProtectionMode::LegacyOrPukBlocked
);
assert_eq!(
mode(&[0x80, 0x07, 0x81, 0x01, 0x02, 0x82, 0x02, 0xAA, 0xBB]),
ProtectionMode::Derived
);
assert_eq!(mode(&[0x80, 0x03, 0x81, 0x01, 0x04]), ProtectionMode::None);
}
#[test]
fn admin_data_round_trip_preserves_other_tags() {
let raw = [
0x80, 0x0C, 0x81, 0x01, 0x02, 0x83, 0x04, 0x01, 0x02, 0x03, 0x04, 0x90, 0x01, 0x7F,
];
let admin = AdminData::parse(&raw).unwrap();
assert_eq!(admin.flags, Some(0x02));
assert_eq!(
admin.other,
vec![(0x83, vec![1, 2, 3, 4]), (0x90, vec![0x7F])]
);
assert_eq!(admin.to_bytes(), raw.to_vec());
}
#[test]
fn admin_data_rejects_malformed_content() {
assert!(AdminData::parse(&[0x80, 0x05, 0x81]).is_err());
assert!(AdminData::parse(&[0x53, 0x03, 0x81, 0x01, 0x02]).is_err());
assert!(AdminData::parse(&[0x80, 0x04, 0x81, 0x02, 0x02, 0x00]).is_err());
}
#[cfg(feature = "virtual-piv")]
mod admin_rewrite {
use super::*;
use crate::piv::VirtualPiv;
const MGMT: &str = DEFAULT_MANAGEMENT_KEY;
fn card_with_admin(raw: &[u8]) -> (VirtualPiv, String) {
let piv = VirtualPiv::new();
let reader = piv.reader_name();
piv.write_object(&reader, OBJ_ADMIN_DATA, raw, MGMT)
.unwrap();
(piv, reader)
}
#[test]
fn legacy_flag_cleared_when_puk_not_blocked() {
let (piv, reader) = card_with_admin(&[0x80, 0x03, 0x81, 0x01, 0x01]);
let payload = admin_data_with_stored_key(&reader, &piv, true).unwrap();
assert_eq!(payload, vec![0x80, 0x03, 0x81, 0x01, 0x02]);
}
#[test]
fn timestamp_and_unknown_bits_preserved() {
let (piv, reader) = card_with_admin(&[
0x80, 0x09, 0x81, 0x01, 0x10, 0x83, 0x04, 0x01, 0x02, 0x03, 0x04,
]);
let payload = admin_data_with_stored_key(&reader, &piv, false).unwrap();
assert_eq!(
payload,
vec![0x80, 0x09, 0x81, 0x01, 0x12, 0x83, 0x04, 0x01, 0x02, 0x03, 0x04]
);
}
#[test]
fn missing_admin_data_is_created() {
let piv = VirtualPiv::new();
let reader = piv.reader_name();
let payload = admin_data_with_stored_key(&reader, &piv, false).unwrap();
assert_eq!(payload, vec![0x80, 0x03, 0x81, 0x01, 0x02]);
}
#[test]
fn pin_derived_is_refused() {
let (piv, reader) =
card_with_admin(&[0x80, 0x07, 0x81, 0x01, 0x00, 0x82, 0x02, 0xAA, 0xBB]);
assert!(admin_data_with_stored_key(&reader, &piv, false).is_err());
}
}
}