mgm: Generalize TDES logic to enable other algorithms (#625)

Co-authored-by: Jack Grigg <thestr4d@gmail.com>
Co-authored-by: Greg Bowyer <gbowyer@fastmail.co.uk>
This commit is contained in:
Tony Arcieri (iqlusion)
2025-08-22 09:37:41 -06:00
committed by GitHub
parent 7eb7a31a28
commit 1e1fe34734
7 changed files with 275 additions and 145 deletions
+21
View File
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `yubikey::certificate::SelfSigned`
- `yubikey::Error::CertificateBuilder`
- `yubikey::MgmAlgorithmId`
- `yubikey::mgm`:
- `MgmKey::generate_for`
- `MgmKey::get_default`
- `impl AsRef<[u8]> for MgmKey`
### Changed
- MSRV is now 1.81.
@@ -20,12 +24,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `rsa 0.10.0-pre.3`
- `sha2 0.11.0-pre.4`
- `x509-cert 0.3.0-pre.0`
- `yubikey::mgm`:
- `MgmKey::generate` now takes a `rand::TryCryptoRng` argument.
- `MgmKey::generate` now requires the caller to specify the key algorithm via
an `MgmAlgorithmId` parameter.
- Use `MgmKey::generate_for` if you want to generate a key using the
preferred algorithm for a given Yubikey's firmware version.
- `MgmKey::from_bytes` now takes an `Option<MgmAlgorithmId>` argument, to
disambiguate algorithms with the same key length.
- `yubikey::piv`:
- `ManagementAlgorithmId` has been renamed to `SlotAlgorithmId`, and its
`ThreeDes` variant has been replaced by `SlotAlgorithmId::Management`
containing a `yubikey::MgmAlgorithmId`.
- Metadata command returns `Error:NotFound` instead of `Error::GenericError` when the object doesn't exist ([#558]).
### Removed
- `yubikey::mgm`:
- `MgmKey::new` (use `MgmKey::from_bytes(_, Some(MgmAlgorithmId::ThreeDes))`
instead).
- `impl AsRef<[u8; DES_LEN_3DES]> for MgmKey` (use
`impl AsRef<[u8]> for MgmKey` instead).
- `impl Default for MgmKey` (use `MgmKey::get_default` instead).
- `impl TryFrom<&[u8]> for MgmKey` (use `MgmKey::from_bytes` instead).
## 0.8.0 (2023-08-15)
### Added
- `impl Debug for {Context, YubiKey}` ([#457])
Generated
+2
View File
@@ -197,6 +197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a23fa214dea9efd4dacee5a5614646b30216ae0f05d4bb51bafb50e9da1c5be"
dependencies = [
"hybrid-array",
"rand_core",
]
[[package]]
@@ -1078,6 +1079,7 @@ version = "0.8.0"
dependencies = [
"base16ct",
"bitflags 2.5.0",
"cipher",
"der",
"des",
"ecdsa",
+1
View File
@@ -25,6 +25,7 @@ x509-cert = { version = "0.3.0-rc.1", features = ["builder", "hazmat"] }
[dependencies]
bitflags = "2.5.0"
cipher = { version = "0.5.0-rc.0", features = ["rand_core"] }
der = "0.8.0-rc.7"
des = "0.9.0-rc.0"
elliptic-curve = "0.14.0-rc.7"
+185 -89
View File
@@ -35,17 +35,14 @@ use crate::{
metadata::{AdminData, ProtectedData},
piv::{ManagementSlotId, SlotAlgorithmId},
transaction::Transaction,
Error, Result, YubiKey,
Error, Result, Version, YubiKey,
};
use bitflags::bitflags;
use log::error;
use rand_core::{OsRng, RngCore, TryRngCore};
use zeroize::Zeroize;
use des::{
cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit},
TdesEde3,
use cipher::{
typenum::Unsigned, BlockCipherDecrypt, BlockCipherEncrypt, Key, KeyInit, KeySizeUser,
};
use log::error;
use rand::TryCryptoRng;
#[cfg(feature = "untested")]
use {
@@ -56,7 +53,7 @@ use {
TAG_SERIAL, TAG_UNLOCK, TAG_USB_ENABLED, TAG_USB_SUPPORTED, TAG_VERSION,
},
serialization::Tlv,
Serial, Version,
Serial,
},
pbkdf2::pbkdf2_hmac,
sha1::Sha1,
@@ -72,17 +69,22 @@ pub(crate) const APPLET_NAME: &str = "YubiKey MGMT";
#[cfg(feature = "untested")]
pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
pub(crate) const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
/// Size of a DES key
pub(super) const DES_LEN_DES: usize = 8;
const DES_LEN_DES: usize = 8;
/// Size of a 3DES key
pub(crate) const DES_LEN_3DES: usize = DES_LEN_DES * 3;
pub(super) const DES_LEN_3DES: usize = DES_LEN_DES * 3;
pub(crate) const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
#[cfg(feature = "untested")]
const CB_ADMIN_SALT: usize = 16;
/// The default MGM key loaded for both Triple-DES and AES keys
const DEFAULT_MGM_KEY: [u8; 24] = [
1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8,
];
/// Number of PBKDF2 iterations to use when deriving from a password
#[cfg(feature = "untested")]
const ITER_MGM_PBKDF2: u32 = 10000;
@@ -153,41 +155,101 @@ impl MgmAlgorithmId {
///
/// The only supported algorithm for MGM keys is 3DES.
#[derive(Clone)]
pub struct MgmKey([u8; DES_LEN_3DES]);
pub struct MgmKey(MgmKeyKind);
#[derive(Clone)]
enum MgmKeyKind {
Tdes(Key<des::TdesEde3>),
}
impl MgmKey {
/// Generate a random MGM key
pub fn generate() -> Self {
let mut key_bytes = [0u8; DES_LEN_3DES];
let mut rng = OsRng.unwrap_err();
rng.fill_bytes(&mut key_bytes);
Self(key_bytes)
/// Generates a random MGM key for the given algorithm.
pub fn generate(alg: MgmAlgorithmId, rng: &mut impl TryCryptoRng) -> Result<Self> {
match alg {
MgmAlgorithmId::ThreeDes => {
des::TdesEde3::try_generate_key_with_rng(rng).map(MgmKeyKind::Tdes)
}
}
.map_err(|e| {
error!("RNG failure: {}", e);
Error::KeyError
})
.map(Self)
}
/// Create an MGM key from byte slice.
/// Generates a random MGM key using the preferred algorithm for the given Yubikey's
/// firmware version.
pub fn generate_for(yubikey: &YubiKey, rng: &mut impl TryCryptoRng) -> Result<Self> {
match yubikey.version() {
// Initial firmware versions default to 3DES.
Version { major: ..=4, .. }
| Version {
major: 5,
minor: ..=6,
..
} => Self::generate(MgmAlgorithmId::ThreeDes, rng),
// Firmware 5.7.0 and above default to AES-192.
Version {
major: 5,
minor: 7..,
..
}
| Version { major: 6.., .. } => Err(Error::NotSupported),
}
}
/// Parses an MGM key from the given byte slice.
///
/// Returns an error if the slice is the wrong size or the key is weak.
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
bytes.as_ref().try_into()
}
/// Create an MGM key from the given byte array.
/// Returns an error if the slice is an invalid size or the key is weak.
///
/// Returns an error if the key is weak.
pub fn new(key_bytes: [u8; DES_LEN_3DES]) -> Result<Self> {
if TdesEde3::weak_key_test(key_bytes.as_ref()).is_err() {
error!(
"blacklisting key '{:?}' since it's weak (with odd parity)",
&key_bytes
);
return Err(Error::KeyError);
/// If `alg` is `None`, the algorithm will be selected based on the length of the
/// slice, returning an error if there is not a unique match.
pub fn from_bytes(bytes: impl AsRef<[u8]>, alg: Option<MgmAlgorithmId>) -> Result<Self> {
match alg {
Some(alg) => Self::parse_key(alg, bytes),
None => match bytes.as_ref().len() {
DES_LEN_3DES => Self::parse_key(MgmAlgorithmId::ThreeDes, bytes),
_ => Err(Error::ParseError),
},
}
}
Ok(Self(key_bytes))
/// Gets the default management key for the given Yubikey's firmware version.
///
/// Returns an error if the Yubikey's default algorithm is unsupported.
pub fn get_default(yubikey: &YubiKey) -> Result<Self> {
match yubikey.version() {
// Initial firmware versions default to 3DES.
Version { major: ..=4, .. }
| Version {
major: 5,
minor: ..=6,
..
} => Ok(Self(MgmKeyKind::Tdes(DEFAULT_MGM_KEY.into()))),
// Firmware 5.7.0 and above default to AES-192.
Version {
major: 5,
minor: 7..,
..
}
| Version { major: 6.., .. } => Err(Error::NotSupported),
}
}
/// Get derived management key (MGM)
/// Resets the management key for the given YubiKey to the default value for that
/// Yubikey's firmware version.
///
/// This will wipe any metadata related to derived and PIN-protected management keys.
pub fn set_default(yubikey: &mut YubiKey) -> Result<()> {
Self::get_default(yubikey)?.set_manual(yubikey, false)
}
/// Derives a 3DES management key (MGM) from a stored salt.
///
/// # Security
///
/// Warning: PIN-derived mode is not secure. You should not use this technique. It is
/// offered only for backwards compatibility.
#[cfg(feature = "untested")]
pub fn get_derived(yubikey: &mut YubiKey, pin: &[u8]) -> Result<Self> {
let txn = yubikey.begin_transaction()?;
@@ -212,9 +274,10 @@ impl MgmKey {
return Err(Error::GenericError);
}
let mut mgm = [0u8; DES_LEN_3DES];
let mut mgm = Key::<des::TdesEde3>::default();
pbkdf2_hmac::<Sha1>(pin, salt, ITER_MGM_PBKDF2, &mut mgm);
MgmKey::from_bytes(mgm)
des::TdesEde3::weak_key_test(&mgm).map_err(|_| Error::KeyError)?;
Ok(Self(MgmKeyKind::Tdes(mgm)))
}
/// Get protected management key (MGM)
@@ -234,24 +297,17 @@ impl MgmKey {
.get_item(TAG_PROTECTED_MGM)
.inspect_err(|e| error!("could not read protected MGM from metadata (err: {:?})", e))?;
if item.len() != DES_LEN_3DES {
Self::parse_key(alg, item).map_err(|e| match e {
Error::SizeError => {
error!(
"protected data contains MGM, but is the wrong size: {} (expected {})",
"protected data contains MGM, but is the wrong size: {} (expected {:?})",
item.len(),
DES_LEN_3DES
alg,
);
return Err(Error::AuthenticationError);
Error::AuthenticationError
}
MgmKey::from_bytes(item)
}
/// Resets the management key for the given YubiKey to the default value.
///
/// This will wipe any metadata related to derived and PIN-protected management keys.
pub fn set_default(yubikey: &mut YubiKey) -> Result<()> {
MgmKey::default().set_manual(yubikey, false)
_ => e,
})
}
/// Configures the given YubiKey to use this management key.
@@ -380,47 +436,87 @@ impl MgmKey {
Ok(())
}
/// Encrypt with 3DES key
pub(crate) fn encrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
let mut output = input.to_owned();
TdesEde3::new(&self.0.into()).encrypt_block((&mut output).into());
output
/// Returns the ID used to identify the key algorithm with APDU packets.
pub(crate) fn algorithm_id(&self) -> MgmAlgorithmId {
match &self.0 {
MgmKeyKind::Tdes(_) => MgmAlgorithmId::ThreeDes,
}
}
/// Decrypt with 3DES key
pub(crate) fn decrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
let mut output = input.to_owned();
TdesEde3::new(&self.0.into()).decrypt_block((&mut output).into());
output
/// Returns the key size in bytes.
pub(crate) fn key_size(&self) -> u8 {
match &self.0 {
MgmKeyKind::Tdes(_) => <des::TdesEde3 as KeySizeUser>::KeySize::U8,
}
}
/// Parses an MGM key from the given byte slice.
///
/// Returns an error if the algorithm is unsupported, or the slice is the wrong size,
/// or the key is weak.
fn parse_key(alg: MgmAlgorithmId, bytes: impl AsRef<[u8]>) -> Result<Self> {
match alg {
MgmAlgorithmId::ThreeDes => {
let key =
Key::<des::TdesEde3>::try_from(bytes.as_ref()).map_err(|_| Error::SizeError)?;
des::TdesEde3::weak_key_test(&key).map_err(|_| Error::KeyError)?;
Ok(MgmKeyKind::Tdes(key))
}
}
.map(Self)
}
/// Encrypts a block with this key.
///
/// Returns an error if the block is the wrong size.
fn encrypt_block(&self, block: &mut [u8]) -> Result<()> {
match &self.0 {
MgmKeyKind::Tdes(k) => {
des::TdesEde3::new(k).encrypt_block(block.try_into().map_err(|_| Error::SizeError)?)
}
}
Ok(())
}
/// Decrypts a block with this key.
///
/// Returns an error if the block is the wrong size.
fn decrypt_block(&self, block: &mut [u8]) -> Result<()> {
match &self.0 {
MgmKeyKind::Tdes(k) => {
des::TdesEde3::new(k).decrypt_block(block.try_into().map_err(|_| Error::SizeError)?)
}
}
Ok(())
}
/// Given a challenge from a card, decrypts it and return the value
pub(crate) fn card_challenge(&self, challenge: &[u8]) -> Result<Vec<u8>> {
let mut output = challenge.to_owned();
self.decrypt_block(output.as_mut_slice())?;
Ok(output)
}
/// Checks the authentication matches the challenge and auth data
pub(crate) fn check_challenge(&self, challenge: &[u8], auth_data: &[u8]) -> Result<()> {
let mut response = challenge.to_owned();
self.encrypt_block(response.as_mut_slice())?;
use subtle::ConstantTimeEq;
if response.ct_eq(auth_data).unwrap_u8() != 1 {
return Err(Error::AuthenticationError);
}
Ok(())
}
}
impl AsRef<[u8; DES_LEN_3DES]> for MgmKey {
fn as_ref(&self) -> &[u8; DES_LEN_3DES] {
&self.0
impl AsRef<[u8]> for MgmKey {
fn as_ref(&self) -> &[u8] {
match &self.0 {
MgmKeyKind::Tdes(k) => k.as_ref(),
}
}
/// Default MGM key configured on all YubiKeys
impl Default for MgmKey {
fn default() -> Self {
MgmKey([
1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8,
])
}
}
impl Drop for MgmKey {
fn drop(&mut self) {
self.0.zeroize();
}
}
impl<'a> TryFrom<&'a [u8]> for MgmKey {
type Error = Error;
fn try_from(key_bytes: &'a [u8]) -> Result<Self> {
Self::new(key_bytes.try_into().map_err(|_| Error::SizeError)?)
}
}
+6 -6
View File
@@ -5,7 +5,7 @@ use crate::{
apdu::{Apdu, Ins, StatusWords},
consts::{CB_BUF_MAX, CB_OBJ_MAX},
error::{Error, Result},
mgm::{MgmKey, DES_LEN_3DES},
mgm::MgmKey,
otp,
piv::{self, AlgorithmId, SlotId},
serialization::*,
@@ -251,11 +251,11 @@ impl<'tx> Transaction<'tx> {
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
let p2 = if require_touch { 0xfe } else { 0xff };
let mut data = [0u8; DES_LEN_3DES + 3];
data[0] = ALGO_3DES;
data[1] = KEY_CARDMGM;
data[2] = DES_LEN_3DES as u8;
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
data.push(new_key.algorithm_id().into());
data.push(KEY_CARDMGM);
data.push(new_key.key_size());
data.extend_from_slice(new_key.as_ref());
let status_words = Apdu::new(Ins::SetMgmKey)
.params(0xff, p2)
+26 -25
View File
@@ -68,6 +68,7 @@ use {
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
/// 3DES authentication
#[cfg(feature = "untested")]
pub(crate) const ALGO_3DES: u8 = 0x03;
/// Card management key
@@ -410,38 +411,45 @@ impl YubiKey {
}
/// Authenticate to the card using the provided management key (MGM).
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<()> {
pub fn authenticate(&mut self, mgm_key: &MgmKey) -> Result<()> {
let txn = self.begin_transaction()?;
// get a challenge from the card
let challenge = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
let card_response = Apdu::new(Ins::Authenticate)
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data([TAG_DYN_AUTH, 0x02, 0x80, 0x00])
.transmit(&txn, 261)?;
if !challenge.is_success() || challenge.data().len() < 12 {
if !card_response.is_success() || card_response.data().len() < 5 {
return Err(Error::AuthenticationError);
}
// send a response to the cards challenge and a challenge of our own.
let response = mgm_key.decrypt(challenge.data()[4..12].try_into()?);
let card_challenge = mgm_key.card_challenge(&card_response.data()[4..])?;
let challenge_len = card_challenge.len();
let mut data = [0u8; 22];
data[0] = TAG_DYN_AUTH;
data[1] = 20; // 2 + 8 + 2 +8
data[2] = 0x80;
data[3] = 8;
data[4..12].copy_from_slice(&response);
data[12] = 0x81;
data[13] = 8;
// If this exceeds a `u8` then the card is giving us unexpected data.
let auth_len = (2 + challenge_len + 2 + challenge_len)
.try_into()
.map_err(|_| Error::AuthenticationError)?;
let mut data = Vec::with_capacity(4 + challenge_len + 2 + challenge_len);
data.push(TAG_DYN_AUTH);
data.push(auth_len);
data.push(0x80);
data.push(challenge_len as u8);
data.extend_from_slice(&card_challenge);
data.push(0x81);
data.push(challenge_len as u8);
let mut host_challenge = vec![0u8; challenge_len];
let mut rng = OsRng.unwrap_err();
rng.fill_bytes(&mut data[14..22]);
rng.fill_bytes(&mut host_challenge);
let mut challenge = [0u8; 8];
challenge.copy_from_slice(&data[14..22]);
data.extend_from_slice(&host_challenge);
let authentication = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data(data)
.transmit(&txn, 261)?;
@@ -450,14 +458,7 @@ impl YubiKey {
}
// compare the response from the card with our challenge
let response = mgm_key.encrypt(&challenge);
use subtle::ConstantTimeEq;
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
return Err(Error::AuthenticationError);
}
Ok(())
mgm_key.check_challenge(&host_challenge, &authentication.data()[4..])
}
/// Get the PIV keys contained in this YubiKey.
+24 -15
View File
@@ -114,32 +114,37 @@ fn test_verify_pin() {
#[test]
#[ignore]
fn test_set_mgmkey() {
let mut rng = OsRng;
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
// Set a protected management key.
assert!(MgmKey::generate().set_protected(&mut yubikey).is_ok());
assert!(MgmKey::generate_for(&yubikey, &mut rng)
.unwrap()
.set_protected(&mut yubikey)
.is_ok());
let protected = MgmKey::get_protected(&mut yubikey).unwrap();
assert!(yubikey.authenticate(MgmKey::default()).is_err());
assert!(yubikey.authenticate(protected.clone()).is_ok());
assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(&protected).is_ok());
// Set a manual management key.
let manual = MgmKey::generate();
let manual = MgmKey::generate_for(&yubikey, &mut rng).unwrap();
assert!(manual.set_manual(&mut yubikey, false).is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_err());
assert!(yubikey.authenticate(protected.clone()).is_err());
assert!(yubikey.authenticate(manual.clone()).is_ok());
assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(&manual).is_ok());
// Set back to the default management key.
assert!(MgmKey::set_default(&mut yubikey).is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(protected).is_err());
assert!(yubikey.authenticate(manual).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(&manual).is_err());
assert!(yubikey.authenticate(&default_key).is_ok());
}
//
@@ -148,9 +153,10 @@ fn test_set_mgmkey() {
fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -215,8 +221,9 @@ fn generate_self_signed_rsa_cert() {
fn generate_rsa3072() {
let mut yubikey = YUBIKEY.lock().unwrap();
let version = yubikey.version();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -314,9 +321,10 @@ fn test_slot_id_display() {
#[ignore]
fn test_read_metadata() {
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -344,9 +352,10 @@ fn test_read_metadata() {
#[ignore]
fn test_read_metadata_missing_key() {
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
// we assume that at least one of these slots is empty
let slots_to_check = [